// Copyright (c) 2013, 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.

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

// Test unmodifiable collection views.
// The collections should pass through the operations that are allowed,
// an throw on the ones that aren't without affecting the original.

void main() {
  var list = <int>[];
  testUnmodifiableList(list, UnmodifiableListView(list), 'empty');
  list = [42];
  testUnmodifiableList(list, UnmodifiableListView(list), 'single-42');
  list = [7];
  testUnmodifiableList(list, UnmodifiableListView(list), 'single!42');
  list = [1, 42, 10];
  testUnmodifiableList(list, UnmodifiableListView(list), 'three-42');
  list = [1, 7, 10];
  testUnmodifiableList(list, UnmodifiableListView(list), 'three!42');

  list = [];
  testNonGrowableList(list, NonGrowableListView(list), 'empty');
  list = [42];
  testNonGrowableList(list, NonGrowableListView(list), 'single-42');
  list = [7];
  testNonGrowableList(list, NonGrowableListView(list), 'single!42');
  list = [1, 42, 10];
  testNonGrowableList(list, NonGrowableListView(list), 'three-42');
  list = [1, 7, 10];
  testNonGrowableList(list, NonGrowableListView(list), 'three!42');

  var aSet = <int>{};
  testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'empty');
  aSet = {};
  testUnmodifiableSet(aSet, const UnmodifiableSetView.empty(), 'const empty');
  aSet = {42};
  testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'single-42');
  aSet = {7};
  testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'single!42');
  aSet = {1, 42, 10};
  testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'three-42');
  aSet = {1, 7, 10};
  testUnmodifiableSet(aSet, UnmodifiableSetView(aSet), 'three!42');
}

void testUnmodifiableList(List<int> original, List<int> wrapped, String name) {
  name = 'unmodifiable-list-$name';
  testIterable(original, wrapped, name);
  testReadList(original, wrapped, name);
  testNoWriteList(original, wrapped, name);
  testNoChangeLengthList(original, wrapped, name);
}

void testNonGrowableList(List<int> original, List<int> wrapped, String name) {
  name = 'nongrowable-list-$name';
  testIterable(original, wrapped, name);
  testReadList(original, wrapped, name);
  testWriteList(original, wrapped, name);
  testNoChangeLengthList(original, wrapped, name);
}

void testUnmodifiableSet(Set<int> original, Set<int> wrapped, String name) {
  name = 'unmodifiable-set-$name';
  testIterable(original, wrapped, name);
  testReadSet(original, wrapped, name);
  testNoChangeSet(original, wrapped, name);
}

void testIterable(Iterable<int> original, Iterable<int> wrapped, String name) {
  test('$name - any', () {
    expect(wrapped.any((x) => true), equals(original.any((x) => true)));
    expect(wrapped.any((x) => false), equals(original.any((x) => false)));
  });

  test('$name - contains', () {
    expect(wrapped.contains(0), equals(original.contains(0)));
  });

  test('$name - elementAt', () {
    if (original.isEmpty) {
      expect(() => wrapped.elementAt(0), throwsRangeError);
    } else {
      expect(wrapped.elementAt(0), equals(original.elementAt(0)));
    }
  });

  test('$name - every', () {
    expect(wrapped.every((x) => true), equals(original.every((x) => true)));
    expect(wrapped.every((x) => false), equals(original.every((x) => false)));
  });

  test('$name - expand', () {
    expect(
        wrapped.expand((x) => [x, x]), equals(original.expand((x) => [x, x])));
  });

  test('$name - first', () {
    if (original.isEmpty) {
      expect(() => wrapped.first, throwsStateError);
    } else {
      expect(wrapped.first, equals(original.first));
    }
  });

  test('$name - firstWhere', () {
    if (original.isEmpty) {
      expect(() => wrapped.firstWhere((_) => true), throwsStateError);
    } else {
      expect(wrapped.firstWhere((_) => true),
          equals(original.firstWhere((_) => true)));
    }
    expect(() => wrapped.firstWhere((_) => false), throwsStateError);
  });

  test('$name - fold', () {
    expect(wrapped.fold(0, (dynamic x, y) => x + y),
        equals(original.fold(0, (dynamic x, y) => x + y)));
  });

  test('$name - forEach', () {
    var wrapCtr = 0;
    var origCtr = 0;
    // ignore: avoid_function_literals_in_foreach_calls
    wrapped.forEach((x) {
      wrapCtr += x;
    });
    // ignore: avoid_function_literals_in_foreach_calls
    original.forEach((x) {
      origCtr += x;
    });
    expect(wrapCtr, equals(origCtr));
  });

  test('$name - isEmpty', () {
    expect(wrapped.isEmpty, equals(original.isEmpty));
  });

  test('$name - isNotEmpty', () {
    expect(wrapped.isNotEmpty, equals(original.isNotEmpty));
  });

  test('$name - iterator', () {
    Iterator wrapIter = wrapped.iterator;
    Iterator origIter = original.iterator;
    while (origIter.moveNext()) {
      expect(wrapIter.moveNext(), equals(true));
      expect(wrapIter.current, equals(origIter.current));
    }
    expect(wrapIter.moveNext(), equals(false));
  });

  test('$name - join', () {
    expect(wrapped.join(''), equals(original.join('')));
    expect(wrapped.join('-'), equals(original.join('-')));
  });

  test('$name - last', () {
    if (original.isEmpty) {
      expect(() => wrapped.last, throwsStateError);
    } else {
      expect(wrapped.last, equals(original.last));
    }
  });

  test('$name - lastWhere', () {
    if (original.isEmpty) {
      expect(() => wrapped.lastWhere((_) => true), throwsStateError);
    } else {
      expect(wrapped.lastWhere((_) => true),
          equals(original.lastWhere((_) => true)));
    }
    expect(() => wrapped.lastWhere((_) => false), throwsStateError);
  });

  test('$name - length', () {
    expect(wrapped.length, equals(original.length));
  });

  test('$name - map', () {
    expect(wrapped.map((x) => '[$x]'), equals(original.map((x) => '[$x]')));
  });

  test('$name - reduce', () {
    if (original.isEmpty) {
      expect(() => wrapped.reduce((x, y) => x + y), throwsStateError);
    } else {
      expect(wrapped.reduce((x, y) => x + y),
          equals(original.reduce((x, y) => x + y)));
    }
  });

  test('$name - single', () {
    if (original.length != 1) {
      expect(() => wrapped.single, throwsStateError);
    } else {
      expect(wrapped.single, equals(original.single));
    }
  });

  test('$name - singleWhere', () {
    if (original.length != 1) {
      expect(() => wrapped.singleWhere((_) => true), throwsStateError);
    } else {
      expect(wrapped.singleWhere((_) => true),
          equals(original.singleWhere((_) => true)));
    }
    expect(() => wrapped.singleWhere((_) => false), throwsStateError);
  });

  test('$name - skip', () {
    expect(wrapped.skip(0), orderedEquals(original.skip(0)));
    expect(wrapped.skip(1), orderedEquals(original.skip(1)));
    expect(wrapped.skip(5), orderedEquals(original.skip(5)));
  });

  test('$name - skipWhile', () {
    expect(wrapped.skipWhile((x) => true),
        orderedEquals(original.skipWhile((x) => true)));
    expect(wrapped.skipWhile((x) => false),
        orderedEquals(original.skipWhile((x) => false)));
    expect(wrapped.skipWhile((x) => x != 42),
        orderedEquals(original.skipWhile((x) => x != 42)));
  });

  test('$name - take', () {
    expect(wrapped.take(0), orderedEquals(original.take(0)));
    expect(wrapped.take(1), orderedEquals(original.take(1)));
    expect(wrapped.take(5), orderedEquals(original.take(5)));
  });

  test('$name - takeWhile', () {
    expect(wrapped.takeWhile((x) => true),
        orderedEquals(original.takeWhile((x) => true)));
    expect(wrapped.takeWhile((x) => false),
        orderedEquals(original.takeWhile((x) => false)));
    expect(wrapped.takeWhile((x) => x != 42),
        orderedEquals(original.takeWhile((x) => x != 42)));
  });

  test('$name - toList', () {
    expect(wrapped.toList(), orderedEquals(original.toList()));
    expect(wrapped.toList(growable: false),
        orderedEquals(original.toList(growable: false)));
  });

  test('$name - toSet', () {
    expect(wrapped.toSet(), unorderedEquals(original.toSet()));
  });

  test('$name - where', () {
    expect(
        wrapped.where((x) => true), orderedEquals(original.where((x) => true)));
    expect(wrapped.where((x) => false),
        orderedEquals(original.where((x) => false)));
    expect(wrapped.where((x) => x != 42),
        orderedEquals(original.where((x) => x != 42)));
  });
}

void testReadList(List original, List wrapped, String name) {
  test('$name - length', () {
    expect(wrapped.length, equals(original.length));
  });

  test('$name - isEmpty', () {
    expect(wrapped.isEmpty, equals(original.isEmpty));
  });

  test('$name - isNotEmpty', () {
    expect(wrapped.isNotEmpty, equals(original.isNotEmpty));
  });

  test('$name - []', () {
    if (original.isEmpty) {
      expect(() {
        // ignore: unnecessary_statements
        wrapped[0];
      }, throwsRangeError);
    } else {
      expect(wrapped[0], equals(original[0]));
    }
  });

  test('$name - indexOf', () {
    expect(wrapped.indexOf(42), equals(original.indexOf(42)));
  });

  test('$name - lastIndexOf', () {
    expect(wrapped.lastIndexOf(42), equals(original.lastIndexOf(42)));
  });

  test('$name - getRange', () {
    var len = original.length;
    expect(wrapped.getRange(0, len), equals(original.getRange(0, len)));
    expect(wrapped.getRange(len ~/ 2, len),
        equals(original.getRange(len ~/ 2, len)));
    expect(
        wrapped.getRange(0, len ~/ 2), equals(original.getRange(0, len ~/ 2)));
  });

  test('$name - sublist', () {
    var len = original.length;
    expect(wrapped.sublist(0), equals(original.sublist(0)));
    expect(wrapped.sublist(len ~/ 2), equals(original.sublist(len ~/ 2)));
    expect(wrapped.sublist(0, len ~/ 2), equals(original.sublist(0, len ~/ 2)));
  });

  test('$name - asMap', () {
    expect(wrapped.asMap(), equals(original.asMap()));
  });
}

void testNoWriteList(List<int> original, List<int> wrapped, String name) {
  var copy = List.of(original);

  void testThrows(String name, void Function() thunk) {
    test(name, () {
      expect(thunk, throwsUnsupportedError);
      // No modifications happened.
      expect(original, equals(copy));
    });
  }

  testThrows('$name - []= throws', () {
    wrapped[0] = 42;
  });

  testThrows('$name - sort throws', () {
    wrapped.sort();
  });

  testThrows('$name - fillRange throws', () {
    wrapped.fillRange(0, wrapped.length, 42);
  });

  testThrows('$name - setRange throws', () {
    wrapped.setRange(
        0, wrapped.length, Iterable.generate(wrapped.length, (i) => i));
  });

  testThrows('$name - setAll throws', () {
    wrapped.setAll(0, Iterable.generate(wrapped.length, (i) => i));
  });
}

void testWriteList(List<int> original, List wrapped, String name) {
  var copy = List.of(original);

  test('$name - []=', () {
    if (original.isNotEmpty) {
      var originalFirst = original[0];
      wrapped[0] = originalFirst + 1;
      expect(original[0], equals(originalFirst + 1));
      original[0] = originalFirst;
    } else {
      expect(() {
        wrapped[0] = 42;
      }, throwsRangeError);
    }
  });

  test('$name - sort', () {
    var sortCopy = List.of(original);
    sortCopy.sort();
    wrapped.sort();
    expect(original, orderedEquals(sortCopy));
    original.setAll(0, copy);
  });

  test('$name - fillRange', () {
    wrapped.fillRange(0, wrapped.length, 37);
    for (var i = 0; i < original.length; i++) {
      expect(original[i], equals(37));
    }
    original.setAll(0, copy);
  });

  test('$name - setRange', () {
    List reverseList = original.reversed.toList();
    wrapped.setRange(0, wrapped.length, reverseList);
    expect(original, equals(reverseList));
    original.setAll(0, copy);
  });

  test('$name - setAll', () {
    List reverseList = original.reversed.toList();
    wrapped.setAll(0, reverseList);
    expect(original, equals(reverseList));
    original.setAll(0, copy);
  });
}

void testNoChangeLengthList(
    List<int> original, List<int> wrapped, String name) {
  var copy = List.of(original);

  void testThrows(String name, void Function() thunk) {
    test(name, () {
      expect(thunk, throwsUnsupportedError);
      // No modifications happened.
      expect(original, equals(copy));
    });
  }

  testThrows('$name - length= throws', () {
    wrapped.length = 100;
  });

  testThrows('$name - add throws', () {
    wrapped.add(42);
  });

  testThrows('$name - addAll throws', () {
    wrapped.addAll([42]);
  });

  testThrows('$name - insert throws', () {
    wrapped.insert(0, 42);
  });

  testThrows('$name - insertAll throws', () {
    wrapped.insertAll(0, [42]);
  });

  testThrows('$name - remove throws', () {
    wrapped.remove(42);
  });

  testThrows('$name - removeAt throws', () {
    wrapped.removeAt(0);
  });

  testThrows('$name - removeLast throws', () {
    wrapped.removeLast();
  });

  testThrows('$name - removeWhere throws', () {
    wrapped.removeWhere((element) => false);
  });

  testThrows('$name - retainWhere throws', () {
    wrapped.retainWhere((element) => true);
  });

  testThrows('$name - removeRange throws', () {
    wrapped.removeRange(0, wrapped.length);
  });

  testThrows('$name - replaceRange throws', () {
    wrapped.replaceRange(0, wrapped.length, [42]);
  });

  testThrows('$name - clear throws', () {
    wrapped.clear();
  });
}

void testReadSet(Set<int> original, Set<int> wrapped, String name) {
  var copy = Set.of(original);

  test('$name - containsAll', () {
    expect(wrapped.containsAll(copy), isTrue);
    expect(wrapped.containsAll(copy.toList()), isTrue);
    expect(wrapped.containsAll([]), isTrue);
    expect(wrapped.containsAll([42]), equals(original.containsAll([42])));
  });

  test('$name - intersection', () {
    expect(wrapped.intersection({}), isEmpty);
    expect(wrapped.intersection(copy), unorderedEquals(original));
    expect(
        wrapped.intersection({42}), Set.of(original.contains(42) ? [42] : []));
  });

  test('$name - union', () {
    expect(wrapped.union({}), unorderedEquals(original));
    expect(wrapped.union(copy), unorderedEquals(original));
    expect(wrapped.union({42}), equals(original.union({42})));
  });

  test('$name - difference', () {
    expect(wrapped.difference({}), unorderedEquals(original));
    expect(wrapped.difference(copy), isEmpty);
    expect(wrapped.difference({42}), equals(original.difference({42})));
  });
}

void testNoChangeSet(Set<int> original, Set<int> wrapped, String name) {
  var originalElements = original.toList();

  void testThrows(String name, void Function() thunk) {
    test(name, () {
      expect(thunk, throwsUnsupportedError);
      // No modifications happened.
      expect(original.toList(), equals(originalElements));
    });
  }

  testThrows('$name - add throws', () {
    wrapped.add(42);
  });

  testThrows('$name - addAll throws', () {
    wrapped.addAll([42]);
  });

  testThrows('$name - addAll empty throws', () {
    wrapped.addAll([]);
  });

  testThrows('$name - remove throws', () {
    wrapped.remove(42);
  });

  testThrows('$name - removeAll throws', () {
    wrapped.removeAll([42]);
  });

  testThrows('$name - removeAll empty throws', () {
    wrapped.removeAll([]);
  });

  testThrows('$name - retainAll throws', () {
    wrapped.retainAll([42]);
  });

  testThrows('$name - removeWhere throws', () {
    wrapped.removeWhere((_) => false);
  });

  testThrows('$name - retainWhere throws', () {
    wrapped.retainWhere((_) => true);
  });

  testThrows('$name - clear throws', () {
    wrapped.clear();
  });
}
