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

// ignore_for_file: avoid_function_literals_in_foreach_calls

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

/// The initial capacity of queues if the user doesn't specify one.
const capacity = 16;

void main() {
  group('Uint8Queue()', () {
    test('creates an empty Uint8Queue', () {
      expect(Uint8Queue(), isEmpty);
    });

    test('takes an initial capacity', () {
      expect(Uint8Queue(100), isEmpty);
    });
  });

  group('add() adds an element to the end', () {
    forEachInternalRepresentation((queue) {
      queue.add(16);
      expect(queue, equals(oneThrough(capacity)));
    });
  });

  group('addFirst() adds an element to the beginning', () {
    forEachInternalRepresentation((queue) {
      queue.addFirst(0);
      expect(queue, equals([0, ...oneThrough(capacity - 1)]));
    });
  });

  group('removeFirst() removes an element from the beginning', () {
    forEachInternalRepresentation((queue) {
      expect(queue.removeFirst(), equals(1));
      expect(queue, equals(oneThrough(capacity - 1).skip(1)));
    });

    test('throws a StateError for an empty queue', () {
      expect(Uint8Queue().removeFirst, throwsStateError);
    });
  });

  group('removeLast() removes an element from the end', () {
    forEachInternalRepresentation((queue) {
      expect(queue.removeLast(), equals(15));
      expect(queue, equals(oneThrough(capacity - 2)));
    });

    test('throws a StateError for an empty queue', () {
      expect(Uint8Queue().removeLast, throwsStateError);
    });
  });

  group('removeRange()', () {
    group('removes a prefix', () {
      forEachInternalRepresentation((queue) {
        queue.removeRange(0, 5);
        expect(queue, equals(oneThrough(capacity - 1).skip(5)));
      });
    });

    group('removes a suffix', () {
      forEachInternalRepresentation((queue) {
        queue.removeRange(10, 15);
        expect(queue, equals(oneThrough(capacity - 6)));
      });
    });

    group('removes from the middle', () {
      forEachInternalRepresentation((queue) {
        queue.removeRange(5, 10);
        expect(queue, equals([1, 2, 3, 4, 5, 11, 12, 13, 14, 15]));
      });
    });

    group('removes everything', () {
      forEachInternalRepresentation((queue) {
        queue.removeRange(0, 15);
        expect(queue, isEmpty);
      });
    });

    test('throws a RangeError for an invalid range', () {
      expect(() => Uint8Queue().removeRange(0, 1), throwsRangeError);
    });
  });

  group('setRange()', () {
    group('sets a range to the contents of an iterable', () {
      forEachInternalRepresentation((queue) {
        queue.setRange(5, 10, oneThrough(10).map((n) => 100 + n), 2);
        expect(queue,
            [1, 2, 3, 4, 5, 103, 104, 105, 106, 107, 11, 12, 13, 14, 15]);
      });
    });

    group('sets a range to the contents of a list', () {
      forEachInternalRepresentation((queue) {
        queue.setRange(5, 10, oneThrough(10).map((n) => 100 + n).toList(), 2);
        expect(queue,
            [1, 2, 3, 4, 5, 103, 104, 105, 106, 107, 11, 12, 13, 14, 15]);
      });
    });

    group(
        'sets a range to a section of the same queue overlapping at the '
        'beginning', () {
      forEachInternalRepresentation((queue) {
        queue.setRange(5, 10, queue, 2);
        expect(queue, [1, 2, 3, 4, 5, 3, 4, 5, 6, 7, 11, 12, 13, 14, 15]);
      });
    });

    group('sets a range to a section of the same queue overlapping at the end',
        () {
      forEachInternalRepresentation((queue) {
        queue.setRange(5, 10, queue, 6);
        expect(queue, [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 11, 12, 13, 14, 15]);
      });
    });

    test('throws a RangeError for an invalid range', () {
      expect(() => Uint8Queue().setRange(0, 1, [1]), throwsRangeError);
    });
  });

  group('length returns the length', () {
    forEachInternalRepresentation((queue) {
      expect(queue.length, equals(15));
    });
  });

  group('length=', () {
    group('empties', () {
      forEachInternalRepresentation((queue) {
        queue.length = 0;
        expect(queue, isEmpty);
      });
    });

    group('shrinks', () {
      forEachInternalRepresentation((queue) {
        queue.length = 5;
        expect(queue, equals([1, 2, 3, 4, 5]));
      });
    });

    group('grows', () {
      forEachInternalRepresentation((queue) {
        queue.length = 20;
        expect(
            queue,
            equals(oneThrough(capacity - 1) +
                List.filled(20 - (capacity - 1), 0)));
      });
    });

    group('zeroes out existing data', () {
      forEachInternalRepresentation((queue) {
        queue.length = 0;
        queue.length = 15;
        expect(queue, equals([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]));
      });
    });

    test('throws a RangeError if length is less than 0', () {
      expect(() => Uint8Queue().length = -1, throwsRangeError);
    });
  });

  group('[]', () {
    group('returns individual entries', () {
      forEachInternalRepresentation((queue) {
        for (var i = 0; i < capacity - 1; i++) {
          expect(queue[i], equals(i + 1));
        }
      });
    });

    test('throws a RangeError if the index is less than 0', () {
      var queue = Uint8Queue.fromList([1, 2, 3]);
      expect(() => queue[-1], throwsRangeError);
    });

    test(
        'throws a RangeError if the index is greater than or equal to the '
        'length', () {
      var queue = Uint8Queue.fromList([1, 2, 3]);
      expect(() => queue[3], throwsRangeError);
    });
  });

  group('[]=', () {
    group('sets individual entries', () {
      forEachInternalRepresentation((queue) {
        for (var i = 0; i < capacity - 1; i++) {
          queue[i] = 100 + i;
        }
        expect(queue, equals(List.generate(capacity - 1, (i) => 100 + i)));
      });
    });

    test('throws a RangeError if the index is less than 0', () {
      var queue = Uint8Queue.fromList([1, 2, 3]);
      expect(() {
        queue[-1] = 0;
      }, throwsRangeError);
    });

    test(
        'throws a RangeError if the index is greater than or equal to the '
        'length', () {
      var queue = Uint8Queue.fromList([1, 2, 3]);
      expect(() {
        queue[3] = 4;
      }, throwsRangeError);
    });
  });

  group('throws a modification error for', () {
    late Uint8Queue queue;
    setUp(() {
      queue = Uint8Queue.fromList([1, 2, 3]);
    });

    test('add', () {
      expect(() => queue.forEach((_) => queue.add(4)),
          throwsConcurrentModificationError);
    });

    test('addAll', () {
      expect(() => queue.forEach((_) => queue.addAll([4, 5, 6])),
          throwsConcurrentModificationError);
    });

    test('addFirst', () {
      expect(() => queue.forEach((_) => queue.addFirst(0)),
          throwsConcurrentModificationError);
    });

    test('removeFirst', () {
      expect(() => queue.forEach((_) => queue.removeFirst()),
          throwsConcurrentModificationError);
    });

    test('removeLast', () {
      expect(() => queue.forEach((_) => queue.removeLast()),
          throwsConcurrentModificationError);
    });

    test('length=', () {
      expect(() => queue.forEach((_) => queue.length = 1),
          throwsConcurrentModificationError);
    });
  });
}

/// Runs [callback] in multiple tests, all with queues containing numbers from
/// one through 15 in various different internal states.
void forEachInternalRepresentation(void Function(Uint8Queue queue) callback) {
  // Test with a queue whose internal table has plenty of room.
  group("for a queue that's below capacity", () {
    // Test with a queue whose elements are in one contiguous block, so `_head <
    // _tail`.
    test('with contiguous elements', () {
      callback(Uint8Queue(capacity * 2)..addAll(oneThrough(capacity - 1)));
    });

    // Test with a queue whose elements are split across the ends of the table,
    // so `_head > _tail`.
    test('with an internal gap', () {
      var queue = Uint8Queue(capacity * 2);
      for (var i = capacity ~/ 2; i < capacity; i++) {
        queue.add(i);
      }
      for (var i = capacity ~/ 2 - 1; i > 0; i--) {
        queue.addFirst(i);
      }
      callback(queue);
    });
  });

  // Test with a queue whose internal table will need to expand if one more
  // element is added.
  group("for a queue that's at capacity", () {
    test('with contiguous elements', () {
      callback(Uint8Queue()..addAll(oneThrough(capacity - 1)));
    });

    test('with an internal gap', () {
      var queue = Uint8Queue();
      for (var i = capacity ~/ 2; i < capacity; i++) {
        queue.add(i);
      }
      for (var i = capacity ~/ 2 - 1; i > 0; i--) {
        queue.addFirst(i);
      }
      callback(queue);
    });
  });
}

/// Returns a list containing the integers from one through [n].
List<int> oneThrough(int n) => List.generate(n, (i) => i + 1);

/// Returns a matcher that expects that a closure throws a
/// [ConcurrentModificationError].
final throwsConcurrentModificationError =
    throwsA(const TypeMatcher<ConcurrentModificationError>());
