// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// Not all categories are currently used in tests.
// They're retained in case we add more tests.
// ignore_for_file: unreachable_from_main

import 'dart:math';

import 'package:characters/characters.dart';
import 'package:test/test.dart';

import 'src/unicode_tests.dart';
import 'src/various_tests.dart';

late Random random;

void main([List<String>? args]) {
  // Ensure random seed is part of every test failure message,
  // and that it can be reapplied for testing.
  var seed = (args != null && args.isNotEmpty)
      ? int.parse(args[0])
      : Random().nextInt(0x3FFFFFFF);
  random = Random(seed);
  group('[Random Seed: $seed]', tests);

  group('characters', () {
    test('operations', () {
      var flag = '\u{1F1E9}\u{1F1F0}'; // Regional Indicators "DK".
      var string = 'Hi $flag!';
      expect(string.length, 8);
      var cs = gc(string);
      expect(cs.length, 5);
      expect(cs.toList(), ['H', 'i', ' ', flag, '!']);
      expect(cs.skip(2).toString(), ' $flag!');
      expect(cs.skipLast(2).toString(), 'Hi ');
      expect(cs.take(2).toString(), 'Hi');
      expect(cs.takeLast(2).toString(), '$flag!');
      expect(cs.getRange(1, 4).toString(), 'i $flag');
      expect(cs.characterAt(1).toString(), 'i');
      expect(cs.characterAt(3).toString(), flag);

      expect(cs.contains('\u{1F1E9}'), false);
      expect(cs.contains(flag), true);
      expect(cs.contains('$flag!'), false);
      expect(cs.containsAll(gc('$flag!')), true);

      expect(cs.takeWhile((x) => x != ' ').toString(), 'Hi');
      expect(cs.takeLastWhile((x) => x != ' ').toString(), '$flag!');
      expect(cs.skipWhile((x) => x != ' ').toString(), ' $flag!');
      expect(cs.skipLastWhile((x) => x != ' ').toString(), 'Hi ');

      expect(cs.findFirst(gc(''))!.moveBack(), false);
      expect(cs.findFirst(gc(flag))!.current, flag);
      expect(cs.findLast(gc(flag))!.current, flag);
      expect(cs.iterator.moveNext(), true);
      expect(cs.iterator.moveBack(), false);
      expect((cs.iterator..moveNext()).current, 'H');
      expect(cs.iteratorAtEnd.moveNext(), false);
      expect(cs.iteratorAtEnd.moveBack(), true);
      expect((cs.iteratorAtEnd..moveBack()).current, '!');
    });

    testParts(gc('a'), gc('b'), gc('c'), gc('d'), gc('e'));

    // Composite pictogram example, from https://en.wikipedia.org/wiki/Zero-width_joiner.
    var flag = '\u{1f3f3}'; // U+1F3F3, Flag, waving. Category Pictogram.
    var white = '\ufe0f'; // U+FE0F, Variant selector 16. Category Extend.
    var zwj = '\u200d'; // U+200D, ZWJ
    var rainbow = '\u{1f308}'; // U+1F308, Rainbow. Category Pictogram

    testParts(
      gc('$flag$white$zwj$rainbow'),
      gc('$flag$white'),
      gc(rainbow),
      gc('$flag$zwj$rainbow'),
      gc('!'),
    );
  });

  group('CharacterRange', () {
    test('new', () {
      var range = CharacterRange('abc');
      expect(range.isEmpty, true);
      expect(range.moveNext(), true);
      expect(range.current, 'a');
    });
    group('new.at', () {
      test('simple', () {
        var range = CharacterRange.at('abc', 0);
        expect(range.isEmpty, true);
        expect(range.moveNext(), true);
        expect(range.current, 'a');

        range = CharacterRange.at('abc', 1);
        expect(range.isEmpty, true);
        expect(range.moveNext(), true);
        expect(range.current, 'b');

        range = CharacterRange.at('abc', 1, 2);
        expect(range.isEmpty, false);
        expect(range.current, 'b');
        expect(range.moveNext(), true);

        range = CharacterRange.at('abc', 0, 3);
        expect(range.isEmpty, false);
        expect(range.current, 'abc');
        expect(range.moveNext(), false);
      });
      test('complicated', () {
        // Composite pictogram example, from https://en.wikipedia.org/wiki/Zero-width_joiner.
        var flag = '\u{1f3f3}'; // U+1F3F3, Flag, waving. Category Pictogram.
        var white = '\ufe0f'; // U+FE0F, Variant selector 16. Category Extend.
        var zwj = '\u200d'; // U+200D, ZWJ
        var rainbow = '\u{1f308}'; // U+1F308, Rainbow. Category Pictogram

        var rainbowFlag = '$flag$white$zwj$rainbow';
        var string = '-$rainbowFlag-';
        var range = CharacterRange.at(string, 1);
        expect(range.isEmpty, true);
        expect(range.moveNext(), true);
        expect(range.current, rainbowFlag);

        range = range = CharacterRange.at(string, 2);
        expect(range.isEmpty, false);
        expect(range.current, rainbowFlag);

        range = range = CharacterRange.at(string, 0, 2);
        expect(range.isEmpty, false);
        expect(range.current, '-$rainbowFlag');

        range = range = CharacterRange.at(string, 0, 2);
        expect(range.isEmpty, false);
        expect(range.current, '-$rainbowFlag');

        range =
            range = CharacterRange.at(string, 2, '-$rainbowFlag'.length - 1);
        expect(range.isEmpty, false);
        expect(range.current, rainbowFlag);
        expect(range.stringBeforeLength, 1);

        range = range = CharacterRange.at(string, 0, string.length);
        expect(range.isEmpty, false);
        expect(range.current, string);
      });
    });
  });
}

void tests() {
  test('empty', () {
    expectGC(gc(''), []);
  });
  group('gc-ASCII', () {
    for (var text in ['', 'A', '123456abcdefab']) {
      test('"$text"', () {
        expectGC(gc(text), charsOf(text));
      });
    }
    test('CR+NL', () {
      expectGC(gc('a\r\nb'), ['a', '\r\n', 'b']);
      expectGC(gc('a\n\rb'), ['a', '\n', '\r', 'b']);
    });
  });
  group('Non-ASCII single-code point', () {
    for (var text in ['à la mode', 'rødgrød-æble-ål']) {
      test('"$text"', () {
        expectGC(gc(text), charsOf(text));
      });
    }
  });
  group('Combining marks', () {
    var text = 'a\u0300 la mode';
    test('"$text"', () {
      expectGC(gc(text), ['a\u0300', ' ', 'l', 'a', ' ', 'm', 'o', 'd', 'e']);
    });
    var text2 = 'æble-a\u030Al';
    test('"$text2"', () {
      expectGC(gc(text2), ['æ', 'b', 'l', 'e', '-', 'a\u030A', 'l']);
    });
  });

  group('Regional Indicators', () {
    test('"🇦🇩🇰🇾🇪🇸"', () {
      // Andorra, Cayman Islands, Spain.
      expectGC(gc('🇦🇩🇰🇾🇪🇸'), ['🇦🇩', '🇰🇾', '🇪🇸']);
    });
    test('"X🇦🇩🇰🇾🇪🇸"', () {
      // Other, Andorra, Cayman Islands, Spain.
      expectGC(gc('X🇦🇩🇰🇾🇪🇸'), ['X', '🇦🇩', '🇰🇾', '🇪🇸']);
    });
    test('"🇩🇰🇾🇪🇸"', () {
      // Denmark, Yemen, unmatched S.
      expectGC(gc('🇩🇰🇾🇪🇸'), ['🇩🇰', '🇾🇪', '🇸']);
    });
    test('"X🇩🇰🇾🇪🇸"', () {
      // Other, Denmark, Yemen, unmatched S.
      expectGC(gc('X🇩🇰🇾🇪🇸'), ['X', '🇩🇰', '🇾🇪', '🇸']);
    });
  });

  group('Hangul', () {
    // Individual characters found on Wikipedia. Not expected to make sense.
    test('"읍쌍된밟"', () {
      expectGC(gc('읍쌍된밟'), ['읍', '쌍', '된', '밟']);
    });
  });

  group('Unicode test', () {
    for (var gcs in splitTests) {
      test('[${testDescription(gcs)}]', () {
        expectGC(gc(gcs.join()), gcs);
      });
    }
  });

  group('Emoji test', () {
    for (var gcs in emojis) {
      test('[${testDescription(gcs)}]', () {
        expectGC(gc(gcs.join()), gcs);
      });
    }
  });

  group('Zalgo test', () {
    for (var gcs in zalgo) {
      test('[${testDescription(gcs)}]', () {
        expectGC(gc(gcs.join()), gcs);
      });
    }
  });
}

// Converts text with no multi-code-point grapheme clusters into
// list of grapheme clusters.
List<String> charsOf(String text) =>
    text.runes.map(String.fromCharCode).toList();

void expectGC(Characters actual, List<String> expected) {
  var text = expected.join();

  // Iterable operations.
  expect(actual.string, text);
  expect(actual.toString(), text);
  expect(actual.toList(), expected);
  expect(actual.length, expected.length);
  if (expected.isNotEmpty) {
    expect(actual.first, expected.first);
    expect(actual.last, expected.last);
  } else {
    expect(() => actual.first, throwsStateError);
    expect(() => actual.last, throwsStateError);
  }
  if (expected.length == 1) {
    expect(actual.single, expected.single);
  } else {
    expect(() => actual.single, throwsStateError);
  }
  expect(actual.isEmpty, expected.isEmpty);
  expect(actual.isNotEmpty, expected.isNotEmpty);
  expect(actual.contains(''), false);
  for (var char in expected) {
    expect(actual.contains(char), true);
  }
  for (var i = 1; i < expected.length; i++) {
    expect(actual.contains(expected[i - 1] + expected[i]), false);
  }
  expect(actual.skip(1).toList(), expected.skip(1).toList());
  expect(actual.take(1).toList(), expected.take(1).toList());
  expect(actual.skip(1).toString(), expected.skip(1).join());
  expect(actual.take(1).toString(), expected.take(1).join());
  expect(actual.getRange(1, 2).toString(), expected.take(2).skip(1).join());

  if (expected.isNotEmpty) {
    expect(
      actual.skipLast(1).toList(),
      expected.take(expected.length - 1).toList(),
    );
    expect(
      actual.takeLast(1).toList(),
      expected.skip(expected.length - 1).toList(),
    );
    expect(
      actual.skipLast(1).toString(),
      expected.take(expected.length - 1).join(),
    );
    expect(
      actual.takeLast(1).toString(),
      expected.skip(expected.length - 1).join(),
    );
  }
  bool isEven(String s) => s.length.isEven;

  expect(
    actual.skipWhile(isEven).toList(),
    expected.skipWhile(isEven).toList(),
  );
  expect(
    actual.takeWhile(isEven).toList(),
    expected.takeWhile(isEven).toList(),
  );
  expect(
    actual.skipWhile(isEven).toString(),
    expected.skipWhile(isEven).join(),
  );
  expect(
    actual.takeWhile(isEven).toString(),
    expected.takeWhile(isEven).join(),
  );

  expect(
    actual.skipLastWhile(isEven).toString(),
    expected.toList().reversed.skipWhile(isEven).toList().reversed.join(),
  );
  expect(
    actual.takeLastWhile(isEven).toString(),
    expected.toList().reversed.takeWhile(isEven).toList().reversed.join(),
  );

  expect(actual.where(isEven).toString(), expected.where(isEven).join());

  expect((actual + actual).toString(), actual.string + actual.string);

  // Iteration.
  var it = actual.iterator;
  expect(it.isEmpty, true);
  for (var i = 0; i < expected.length; i++) {
    expect(it.moveNext(), true);
    expect(it.current, expected[i]);

    expect(actual.elementAt(i), expected[i]);
    expect(actual.skip(i).first, expected[i]);
    expect(actual.characterAt(i).toString(), expected[i]);
    expect(actual.getRange(i, i + 1).toString(), expected[i]);
  }
  expect(it.moveNext(), false);
  for (var i = expected.length - 1; i >= 0; i--) {
    expect(it.moveBack(), true);
    expect(it.current, expected[i]);
  }
  expect(it.moveBack(), false);
  expect(it.isEmpty, true);

  // GraphemeClusters operations.
  expect(actual.toUpperCase().string, text.toUpperCase());
  expect(actual.toLowerCase().string, text.toLowerCase());

  expect(actual.string, text);

  expect(actual.containsAll(gc('')), true);
  expect(actual.containsAll(actual), true);
  if (expected.isNotEmpty) {
    var steps = min(5, expected.length);
    for (var s = 0; s <= steps; s++) {
      var i = expected.length * s ~/ steps;
      expect(actual.startsWith(gc(expected.sublist(0, i).join())), true);
      expect(actual.endsWith(gc(expected.sublist(i).join())), true);
      for (var t = s + 1; t <= steps; t++) {
        var j = expected.length * t ~/ steps;
        var slice = expected.sublist(i, j).join();
        var gcs = gc(slice);
        expect(actual.containsAll(gcs), true);
      }
    }
  }

  {
    // Random walk back and forth.
    var it = actual.iterator;
    var pos = -1;
    if (random.nextBool()) {
      pos = expected.length;
      it = actual.iteratorAtEnd;
    }
    var steps = 5 + random.nextInt(expected.length * 2 + 1);
    var lastMove = false;
    while (true) {
      var back = false;
      if (pos < 0) {
        expect(lastMove, false);
        expect(it.isEmpty, true);
      } else if (pos >= expected.length) {
        expect(lastMove, false);
        expect(it.isEmpty, true);
        back = true;
      } else {
        expect(lastMove, true);
        expect(it.current, expected[pos]);
        back = random.nextBool();
      }
      if (--steps < 0) break;
      if (back) {
        lastMove = it.moveBack();
        pos -= 1;
      } else {
        lastMove = it.moveNext();
        pos += 1;
      }
    }
  }
}

Characters gc(String string) => Characters(string);

void testParts(
  Characters a,
  Characters b,
  Characters c,
  Characters d,
  Characters e,
) {
  var cs = gc('$a$b$c$d$e');
  test('$cs', () {
    var it = cs.iterator;
    expect(it.isEmpty, true);
    expect(it.isNotEmpty, false);
    expect(it.current, '');

    // moveNext().
    expect(it.moveNext(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$a');
    expect(it.moveNext(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$b');
    expect(it.moveNext(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$c');
    expect(it.moveNext(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$d');
    expect(it.moveNext(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$e');
    expect(it.moveNext(), false);
    expect(it.isEmpty, true);
    expect(it.current, '');

    // moveBack().
    expect(it.moveBack(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$e');
    expect(it.moveBack(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$d');
    expect(it.moveBack(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$c');
    expect(it.moveBack(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$b');
    expect(it.moveBack(), true);
    expect(it.isEmpty, false);
    expect(it.current, '$a');
    expect(it.moveBack(), false);
    expect(it.isEmpty, true);
    expect(it.current, '');

    // moveNext(int).
    expect(it.moveTo(c), true);
    expect(it.current, '$c');
    expect(it.moveTo(b), false);
    expect(it.moveTo(c), false);
    expect(it.current, '$c');
    expect(it.moveTo(d), true);
    expect(it.current, '$d');

    // moveBack(c).
    expect(it.moveBackTo(c), true);
    expect(it.current, '$c');
    expect(it.moveBackTo(d), false);
    expect(it.moveBackTo(c), false);
    expect(it.moveBackTo(a), true);
    expect(it.current, '$a');

    // moveNext(n)
    expect(it.moveBack(), false);

    expect(it.moveNext(2), true);
    expect(it.current, '$a$b');
    expect(it.moveNext(4), false);
    expect(it.current, '$c$d$e');
    expect(it.moveNext(0), true);
    expect(it.current, '');
    expect(it.moveNext(1), false);
    expect(it.current, '');

    // moveBack(n).
    expect(it.moveBack(2), true);
    expect(it.current, '$d$e');
    expect(it.moveBack(1), true);
    expect(it.current, '$c');
    expect(it.moveBack(3), false);
    expect(it.current, '$a$b');
    expect(it.moveBack(), false);

    // moveFirst.
    it.expandAll();
    expect(it.current, '$a$b$c$d$e');
    expect(it.collapseToFirst(b), true);
    expect(it.current, '$b');
    it.expandAll();
    expect(it.current, '$b$c$d$e');
    expect(it.collapseToFirst(a), false);
    expect(it.current, '$b$c$d$e');

    // moveBackTo
    it.expandBackAll();
    expect(it.current, '$a$b$c$d$e');
    expect(it.collapseToLast(c), true);
    expect(it.current, '$c');

    // includeNext/includePrevious
    expect(it.expandTo(e), true);
    expect(it.current, '$c$d$e');
    expect(it.expandTo(e), false);
    expect(it.expandBackTo(b), true);
    expect(it.current, '$b$c$d$e');
    expect(it.expandBackTo(b), false);
    expect(it.current, '$b$c$d$e');
    expect(it.collapseToFirst(c), true);
    expect(it.current, '$c');

    // includeUntilNext/expandBackUntil
    expect(it.expandBackUntil(a), true);
    expect(it.current, '$b$c');
    expect(it.expandBackUntil(a), true);
    expect(it.current, '$b$c');
    expect(it.expandUntil(e), true);
    expect(it.current, '$b$c$d');
    expect(it.expandUntil(e), true);
    expect(it.current, '$b$c$d');

    // dropFirst/dropLast
    expect(it.dropFirst(), true);
    expect(it.current, '$c$d');
    expect(it.dropLast(), true);
    expect(it.current, '$c');
    it.expandBackAll();
    it.expandAll();
    expect(it.current, '$a$b$c$d$e');
    expect(it.dropTo(b), true);
    expect(it.current, '$c$d$e');
    expect(it.dropBackTo(d), true);
    expect(it.current, '$c');

    it.expandBackAll();
    it.expandAll();
    expect(it.current, '$a$b$c$d$e');

    expect(it.dropUntil(b), true);
    expect(it.current, '$b$c$d$e');
    expect(it.dropBackUntil(d), true);
    expect(it.current, '$b$c$d');

    it.dropWhile((x) => x == b.string);
    expect(it.current, '$c$d');
    it.expandBackAll();
    expect(it.current, '$a$b$c$d');
    it.dropBackWhile((x) => x != b.string);
    expect(it.current, '$a$b');
    it.dropBackWhile((x) => false);
    expect(it.current, '$a$b');

    // include..While
    it.expandWhile((x) => false);
    expect(it.current, '$a$b');
    it.expandWhile((x) => x != e.string);
    expect(it.current, '$a$b$c$d');
    expect(it.collapseToFirst(c), true);
    expect(it.current, '$c');
    it.expandBackWhile((x) => false);
    expect(it.current, '$c');
    it.expandBackWhile((x) => x != a.string);
    expect(it.current, '$b$c');

    var cs2 = cs.replaceAll(c, gc(''));
    var cs3 = cs.replaceFirst(c, gc(''));
    var cs4 = cs.findFirst(c)!.replaceRange(gc('')).source;
    var cse = gc('$a$b$d$e');
    expect(cs2, cse);
    expect(cs3, cse);
    expect(cs4, cse);
    var cs5 = cs4.replaceAll(a, c);
    expect(cs5, gc('$c$b$d$e'));
    var cs6 = cs5.replaceAll(gc(''), a);
    expect(cs6, gc('$a$c$a$b$a$d$a$e$a'));
    var cs7 = cs6.replaceFirst(b, a);
    expect(cs7, gc('$a$c$a$a$a$d$a$e$a'));
    var cs8 = cs7.replaceFirst(e, a);
    expect(cs8, gc('$a$c$a$a$a$d$a$a$a'));
    var cs9 = cs8.replaceAll(a + a, b);
    expect(cs9, gc('$a$c$b$a$d$b$a'));
    it = cs9.iterator;
    it.moveTo(b + a);
    expect('$b$a', it.current);
    it.expandTo(b + a);
    expect('$b$a$d$b$a', it.current);
    var cs10 = it.replaceAll(b + a, e + e)!;
    expect(cs10.currentCharacters, e + e + d + e + e);
    expect(cs10.source, gc('$a$c$e$e$d$e$e'));
    var cs11 = it.replaceRange(e);
    expect(cs11.currentCharacters, e);
    expect(cs11.source, gc('$a$c$e'));

    var cs12 = gc('$a$b$a');
    expect(cs12.split(b), [a, a]);
    expect(cs12.split(a), [gc(''), b, gc('')]);
    expect(cs12.split(a, 2), [gc(''), gc('$b$a')]);

    expect(cs12.split(gc('')), [a, b, a]);
    expect(cs12.split(gc(''), 2), [a, gc('$b$a')]);

    expect(gc('').split(gc('')), [gc('')]);

    var cs13 = gc('$b$a$b$a$b$a');
    expect(cs13.split(b), [gc(''), a, a, a]);
    expect(cs13.split(b, 1), [cs13]);
    expect(cs13.split(b, 2), [gc(''), gc('$a$b$a$b$a')]);
    expect(cs13.split(b, 3), [gc(''), a, gc('$a$b$a')]);
    expect(cs13.split(b, 4), [gc(''), a, a, a]);
    expect(cs13.split(b, 5), [gc(''), a, a, a]);
    expect(cs13.split(b, 9999), [gc(''), a, a, a]);
    expect(cs13.split(b, 0), [gc(''), a, a, a]);
    expect(cs13.split(b, -1), [gc(''), a, a, a]);
    expect(cs13.split(b, -9999), [gc(''), a, a, a]);

    it = cs13.iterator..expandAll();
    expect(it.current, '$b$a$b$a$b$a');
    it.dropFirst();
    it.dropLast();
    expect(it.current, '$a$b$a$b');
    expect(it.split(a).map((range) => range.current), ['', '$b', '$b']);
    expect(it.split(a, 2).map((range) => range.current), ['', '$b$a$b']);
    // Each split is after an *a*.
    var first = true;
    for (var range in it.split(a)) {
      if (range.isEmpty) {
        // First range is empty.
        expect(first, true);
        first = false;
        continue;
      }
      // Later ranges are "b" that come after "a".
      expect(range.current, '$b');
      range.moveBack();
      expect(range.current, '$a');
    }

    expect(it.split(gc('')).map((range) => range.current), [
      '$a',
      '$b',
      '$a',
      '$b',
    ]);

    expect(gc('').iterator.split(gc('')).map((range) => range.current), ['']);

    expect(cs.startsWith(gc('')), true);
    expect(cs.startsWith(a), true);
    expect(cs.startsWith(a + b), true);
    expect(cs.startsWith(gc('$a$b$c')), true);
    expect(cs.startsWith(gc('$a$b$c$d')), true);
    expect(cs.startsWith(gc('$a$b$c$d$e')), true);
    expect(cs.startsWith(b), false);
    expect(cs.startsWith(c), false);
    expect(cs.startsWith(d), false);
    expect(cs.startsWith(e), false);

    expect(cs.endsWith(gc('')), true);
    expect(cs.endsWith(e), true);
    expect(cs.endsWith(d + e), true);
    expect(cs.endsWith(gc('$c$d$e')), true);
    expect(cs.endsWith(gc('$b$c$d$e')), true);
    expect(cs.endsWith(gc('$a$b$c$d$e')), true);
    expect(cs.endsWith(d), false);
    expect(cs.endsWith(c), false);
    expect(cs.endsWith(b), false);
    expect(cs.endsWith(a), false);

    it = cs.findFirst(b + c)!;
    expect(it.startsWith(gc('')), true);
    expect(it.startsWith(b), true);
    expect(it.startsWith(b + c), true);
    expect(it.startsWith(a + b + c), false);
    expect(it.startsWith(b + c + d), false);
    expect(it.startsWith(a), false);

    expect(it.endsWith(gc('')), true);
    expect(it.endsWith(c), true);
    expect(it.endsWith(b + c), true);
    expect(it.endsWith(a + b + c), false);
    expect(it.endsWith(b + c + d), false);
    expect(it.endsWith(d), false);

    it.collapseToFirst(c);
    expect(it.isPrecededBy(gc('')), true);
    expect(it.isPrecededBy(b), true);
    expect(it.isPrecededBy(a + b), true);
    expect(it.isPrecededBy(a + b + c), false);
    expect(it.isPrecededBy(a), false);

    expect(it.isFollowedBy(gc('')), true);
    expect(it.isFollowedBy(d), true);
    expect(it.isFollowedBy(d + e), true);
    expect(it.isFollowedBy(c + d + e), false);
    expect(it.isFollowedBy(e), false);
  });
  test('replace methods', () {
    // Unicode grapheme breaking character classes,
    // represented by their first value.

    var pattern = gc('\t'); // A non-combining entry to be replaced.
    var non = gc('');

    var c = otr + cr + pattern + lf + pic + pattern + zwj + pic + otr;
    var r = c.replaceAll(pattern, non);
    expect(r, otr + cr + lf + pic + zwj + pic + otr);
    var ci = c.iterator..moveNextAll();
    var ri = ci.replaceAll(pattern, non)!;
    expect(ri.currentCharacters, otr + cr + lf + pic + zwj + pic + otr);
    ci.dropFirst();
    ci.dropLast();
    expect(ci.currentCharacters, cr + pattern + lf + pic + pattern + zwj + pic);
    expect(ci.currentCharacters.length, 7);
    ri = ci.replaceAll(pattern, non)!;
    expect(ri.currentCharacters, cr + lf + pic + zwj + pic);
    expect(ri.currentCharacters.length, 2);
    ci.dropFirst();
    ci.dropLast(5);
    expect(ci.currentCharacters, pattern);
    ri = ci.replaceAll(pattern, non)!;
    expect(ri.currentCharacters, cr + lf);
    ci.moveNext(2);
    ci.moveNext(1);
    expect(ci.currentCharacters, pattern);
    ri = ci.replaceAll(pattern, non)!;
    expect(ri.currentCharacters, pic + zwj + pic);

    c = otr + pic + ext + pattern + pic + ext + otr;
    expect(c.length, 5);
    ci = c.iterator..moveTo(pattern);
    expect(ci.currentCharacters, pattern);
    ri = ci.replaceAll(pattern, zwj)!;
    expect(ri.currentCharacters, pic + ext + zwj + pic + ext);

    c = reg + pattern + reg + reg;
    ci = c.iterator..moveTo(pattern);
    ri = ci.replaceRange(non);
    expect(ri.currentCharacters, reg + reg);
    expect(ri.moveNext(), true);
    expect(ri.currentCharacters, reg);

    c = inc + ine + pattern + ine + zwj + inc + ext;
    // Breaks before and after `pattern`, before second `inc`.
    expect(c.length, 4);
    ci = c.iterator..moveTo(pattern);
    ri = ci.replaceRange(non);
    // Still breaks before second `inc`.
    expect(ri.currentCharacters, inc + ine + ine + zwj);
    expect(ri.source.length, 2);

    ci = c.iterator..moveTo(pattern);
    ri = ci.replaceRange(inl);
    expect(ri.currentCharacters, inc + ine + inl + ine + zwj + inc + ext);
    expect(ri.source.length, 1);
  });
}

// Sample characters from each breaking algorithm category.
// TODO: Generate these.
final Characters ctl = gc('\x00'); // Control, NUL.
final Characters cr = gc('\r'); // Carriage Return, CR.
final Characters lf = gc('\n'); // Newline, NL.
final Characters otr = gc(' '); // Other, Space.
final Characters ext = gc('\u200c'); // Extend, Combining Grave Accent.
final Characters spc = gc('\u0903'); // Spacing Mark, Devanagari Sign Visarga.
final Characters pre = gc('\u0600'); // Prepend, Arabic Number Sign.
final Characters zwj = gc('\u200d'); // Zero-Width Joiner.
final Characters pic = gc('\u00a9'); // Extended Pictographic, Copyright.
final Characters reg = gc('\u{1f1e6}'); // Regional Identifier "a".
final Characters hanl = gc('\u1100'); // Hangul L, Choseong Kiyeok.
final Characters hanv = gc('\u1160'); // Hangul V, Jungseong Filler.
final Characters hant = gc('\u11a8'); // Hangul T, Jongseong Kiyeok.
final Characters hanlv = gc('\uac00'); // Hangul LV, Syllable Ga.
final Characters hanlvt = gc('\uac01'); // Hangul LVT, Syllable Gag.
final Characters inc = gc(
  '\u0915',
); // Other{InCL=Consonant}, Devanagari letter Ka.
final Characters ine = gc(
  '\u0300',
); // Extend{InCL=Extend}, Combining Grave Accent.
final Characters inl = gc(
  '\u094d',
); // Extend{InCL=Linker}, Devanagari Sign Virama.
