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

/// Tests algorithm utilities.
import 'dart:math';

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

void main() {
  void testShuffle(List list) {
    var copy = list.toList();
    shuffle(list);
    expect(UnorderedIterableEquality().equals(list, copy), isTrue);
  }

  test('Shuffle 0', () {
    testShuffle([]);
  });
  test('Shuffle 1', () {
    testShuffle([1]);
  });
  test('Shuffle 3', () {
    testShuffle([1, 2, 3]);
  });
  test('Shuffle 10', () {
    testShuffle([1, 2, 3, 4, 5, 1, 3, 5, 7, 9]);
  });
  test('Shuffle shuffles', () {
    var l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
    var c = l.toList();
    var count = 0;
    for (;;) {
      shuffle(l);
      if (!const ListEquality().equals(c, l)) return;
      // Odds of not changing the order should be one in ~ 16! ~= 2e+13.
      // Repeat this 10 times, and the odds of accidentally shuffling to the
      // same result every time is disappearingly tiny.
      count++;
      // If this happens even once, it's ok to report it.
      print('Failed shuffle $count times');
      if (count == 10) fail("Shuffle didn't change order.");
    }
  });
  test('Shuffle sublist', () {
    var l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
    var c = l.toList();
    shuffle(l, 4, 12);
    expect(const IterableEquality().equals(l.getRange(0, 4), c.getRange(0, 4)),
        isTrue);
    expect(
        const IterableEquality().equals(l.getRange(12, 16), c.getRange(12, 16)),
        isTrue);
    expect(
        const UnorderedIterableEquality()
            .equals(l.getRange(4, 12), c.getRange(4, 12)),
        isTrue);
  });

  test('binsearch0', () {
    expect(binarySearch([], 2), equals(-1));
  });

  test('binsearch1', () {
    expect(binarySearch([5], 2), equals(-1));
    expect(binarySearch([5], 5), equals(0));
    expect(binarySearch([5], 7), equals(-1));
  });

  test('binsearch3', () {
    expect(binarySearch([0, 5, 10], -1), equals(-1));
    expect(binarySearch([0, 5, 10], 0), equals(0));
    expect(binarySearch([0, 5, 10], 2), equals(-1));
    expect(binarySearch([0, 5, 10], 5), equals(1));
    expect(binarySearch([0, 5, 10], 7), equals(-1));
    expect(binarySearch([0, 5, 10], 10), equals(2));
    expect(binarySearch([0, 5, 10], 12), equals(-1));
  });

  test('binsearchCompare0', () {
    expect(binarySearch(<C>[], C(2), compare: compareC), equals(-1));
  });

  test('binsearchCompare1', () {
    var l1 = [C(5)];
    expect(binarySearch(l1, C(2), compare: compareC), equals(-1));
    expect(binarySearch(l1, C(5), compare: compareC), equals(0));
    expect(binarySearch(l1, C(7), compare: compareC), equals(-1));
  });

  test('binsearchCompare3', () {
    var l3 = [C(0), C(5), C(10)];
    expect(binarySearch(l3, C(-1), compare: compareC), equals(-1));
    expect(binarySearch(l3, C(0), compare: compareC), equals(0));
    expect(binarySearch(l3, C(2), compare: compareC), equals(-1));
    expect(binarySearch(l3, C(5), compare: compareC), equals(1));
    expect(binarySearch(l3, C(7), compare: compareC), equals(-1));
    expect(binarySearch(l3, C(10), compare: compareC), equals(2));
    expect(binarySearch(l3, C(12), compare: compareC), equals(-1));
  });

  test('lowerbound0', () {
    expect(lowerBound([], 2), equals(0));
  });

  test('lowerbound1', () {
    expect(lowerBound([5], 2), equals(0));
    expect(lowerBound([5], 5), equals(0));
    expect(lowerBound([5], 7), equals(1));
  });

  test('lowerbound3', () {
    expect(lowerBound([0, 5, 10], -1), equals(0));
    expect(lowerBound([0, 5, 10], 0), equals(0));
    expect(lowerBound([0, 5, 10], 2), equals(1));
    expect(lowerBound([0, 5, 10], 5), equals(1));
    expect(lowerBound([0, 5, 10], 7), equals(2));
    expect(lowerBound([0, 5, 10], 10), equals(2));
    expect(lowerBound([0, 5, 10], 12), equals(3));
  });

  test('lowerboundRepeat', () {
    expect(lowerBound([5, 5, 5], 5), equals(0));
    expect(lowerBound([0, 5, 5, 5, 10], 5), equals(1));
  });

  test('lowerboundCompare0', () {
    expect(lowerBound(<C>[], C(2), compare: compareC), equals(0));
  });

  test('lowerboundCompare1', () {
    var l1 = [C(5)];
    expect(lowerBound(l1, C(2), compare: compareC), equals(0));
    expect(lowerBound(l1, C(5), compare: compareC), equals(0));
    expect(lowerBound(l1, C(7), compare: compareC), equals(1));
  });

  test('lowerboundCompare3', () {
    var l3 = [C(0), C(5), C(10)];
    expect(lowerBound(l3, C(-1), compare: compareC), equals(0));
    expect(lowerBound(l3, C(0), compare: compareC), equals(0));
    expect(lowerBound(l3, C(2), compare: compareC), equals(1));
    expect(lowerBound(l3, C(5), compare: compareC), equals(1));
    expect(lowerBound(l3, C(7), compare: compareC), equals(2));
    expect(lowerBound(l3, C(10), compare: compareC), equals(2));
    expect(lowerBound(l3, C(12), compare: compareC), equals(3));
  });

  test('lowerboundCompareRepeat', () {
    var l1 = [C(5), C(5), C(5)];
    var l2 = [C(0), C(5), C(5), C(5), C(10)];
    expect(lowerBound(l1, C(5), compare: compareC), equals(0));
    expect(lowerBound(l2, C(5), compare: compareC), equals(1));
  });

  void testSort(String name,
      void Function(List<int> elements, [int? start, int? end]) sort) {
    test('${name}Random', () {
      var random = Random();
      for (var i = 0; i < 250; i += 10) {
        var list = [
          for (var j = 0; j < i; j++)
            random.nextInt(25) // Expect some equal elements.
        ];
        sort(list);
        for (var j = 1; j < i; j++) {
          expect(list[j - 1], lessThanOrEqualTo(list[j]));
        }
      }
    });

    test('${name}SubRanges', () {
      var l = [6, 5, 4, 3, 2, 1];
      sort(l, 2, 4);
      expect(l, equals([6, 5, 3, 4, 2, 1]));
      sort(l, 1, 1);
      expect(l, equals([6, 5, 3, 4, 2, 1]));
      sort(l, 4, 6);
      expect(l, equals([6, 5, 3, 4, 1, 2]));
      sort(l, 0, 2);
      expect(l, equals([5, 6, 3, 4, 1, 2]));
      sort(l, 0, 6);
      expect(l, equals([1, 2, 3, 4, 5, 6]));
    });

    test('$name insertionSortSpecialCases', () {
      var l = [6, 6, 6, 6, 6, 6];
      sort(l);
      expect(l, equals([6, 6, 6, 6, 6, 6]));

      l = [6, 6, 3, 3, 0, 0];
      sort(l);
      expect(l, equals([0, 0, 3, 3, 6, 6]));
    });
  }

  int intId(int x) => x;
  int intCompare(int a, int b) => a - b;
  testSort('insertionSort', (list, [start, end]) {
    insertionSortBy(list, intId, intCompare, start ?? 0, end ?? list.length);
  });
  testSort('mergeSort compare', (list, [start, end]) {
    mergeSort(list,
        start: start ?? 0, end: end ?? list.length, compare: intCompare);
  });
  testSort('mergeSort comparable', (list, [start, end]) {
    mergeSort(list, start: start ?? 0, end: end ?? list.length);
  });
  testSort('mergeSortBy', (list, [start, end]) {
    mergeSortBy(list, intId, intCompare, start ?? 0, end ?? list.length);
  });
  testSort('quickSort', (list, [start, end]) {
    quickSort(list, intCompare, start ?? 0, end ?? list.length);
  });
  testSort('quickSortBy', (list, [start, end]) {
    quickSortBy(list, intId, intCompare, start ?? 0, end ?? list.length);
  });
  test('MergeSortSpecialCases', () {
    for (var size in [511, 512, 513]) {
      // All equal.
      var list = List<OC>.generate(size, (i) => OC(0, i));
      mergeSort(list);
      for (var i = 0; i < size; i++) {
        expect(list[i].order, equals(i));
      }
      // All but one equal, first.
      list[0] = OC(1, 0);
      for (var i = 1; i < size; i++) {
        list[i] = OC(0, i);
      }
      mergeSort(list);
      for (var i = 0; i < size - 1; i++) {
        expect(list[i].order, equals(i + 1));
      }
      expect(list[size - 1].order, equals(0));

      // All but one equal, last.
      for (var i = 0; i < size - 1; i++) {
        list[i] = OC(0, i);
      }
      list[size - 1] = OC(-1, size - 1);
      mergeSort(list);
      expect(list[0].order, equals(size - 1));
      for (var i = 1; i < size; i++) {
        expect(list[i].order, equals(i - 1));
      }

      // Reversed.
      for (var i = 0; i < size; i++) {
        list[i] = OC(size - 1 - i, i);
      }
      mergeSort(list);
      for (var i = 0; i < size; i++) {
        expect(list[i].id, equals(i));
        expect(list[i].order, equals(size - 1 - i));
      }
    }
  });

  void testSortBy(
      String name,
      void Function<T, K>(List<T> elements, K Function(T element) keyOf,
              int Function(K a, K b) compare,
              [int start, int end])
          sort) {
    for (var n in [0, 1, 2, 10, 75, 250]) {
      var name2 = name;
      test('$name2: Same #$n', () {
        var list = List<OC>.generate(n, (i) => OC(i, 0));
        // Should succeed. Bad implementations of, e.g., quicksort can diverge.
        sort(list, ocOrder, compareInt);
      });
      test('$name: Pre-sorted #$n', () {
        var list = List<OC>.generate(n, (i) => OC(-i, i));
        var expected = list.toList();
        sort(list, ocOrder, compareInt);
        // Elements have not moved.
        expect(list, expected);
      });
      test('$name: Reverse-sorted #$n', () {
        var list = List<OC>.generate(n, (i) => OC(i, -i));
        sort(list, ocOrder, compareInt);
        expectSorted(list, ocOrder, compareInt);
      });
      test('$name: Random #$n', () {
        var random = Random();
        var list = List<OC>.generate(n, (i) => OC(i, random.nextInt(n)));
        sort(list, ocOrder, compareInt);
        expectSorted(list, ocOrder, compareInt);
      });
      test('$name: Sublist #$n', () {
        var random = Random();
        var list = List<OC>.generate(n, (i) => OC(i, random.nextInt(n)));
        var original = list.toList();
        var start = n ~/ 4;
        var end = start * 3;
        sort(list, ocOrder, compareInt, start, end);
        expectSorted(list, ocOrder, compareInt, start, end);
        expect(list.sublist(0, start), original.sublist(0, start));
        expect(list.sublist(end), original.sublist(end));
      });
    }
  }

  testSortBy('insertionSort', insertionSortBy);
  testSortBy('mergeSort', mergeSortBy);
  testSortBy('quickSortBy', quickSortBy);

  test('MergeSortPreservesOrder', () {
    var random = Random();
    // Small case where only insertion call is called,
    // larger case where the internal moving insertion sort is used
    // larger cases with multiple splittings, numbers just around a power of 2.
    for (var size in [8, 50, 511, 512, 513]) {
      // Class OC compares using id.
      // With size elements with id's in the range 0..size/4, a number of
      // collisions are guaranteed. These should be sorted so that the 'order'
      // part of the objects are still in order.
      var list = [
        for (var i = 0; i < size; i++) OC(random.nextInt(size >> 2), i)
      ];
      mergeSort(list);
      var prev = list[0];
      for (var i = 1; i < size; i++) {
        var next = list[i];
        expect(prev.id, lessThanOrEqualTo(next.id));
        if (next.id == prev.id) {
          expect(prev.order, lessThanOrEqualTo(next.order));
        }
        prev = next;
      }
      // Reverse compare on part of list.
      List copy = list.toList();
      var min = size >> 2;
      var max = size - min;
      mergeSort<OC>(list,
          start: min, end: max, compare: (a, b) => b.compareTo(a));
      prev = list[min];
      for (var i = min + 1; i < max; i++) {
        var next = list[i];
        expect(prev.id, greaterThanOrEqualTo(next.id));
        if (next.id == prev.id) {
          expect(prev.order, lessThanOrEqualTo(next.order));
        }
        prev = next;
      }
      // Equals on OC objects is identity, so this means the parts before min,
      // and the parts after max, didn't change at all.
      expect(list.sublist(0, min), equals(copy.sublist(0, min)));
      expect(list.sublist(max), equals(copy.sublist(max)));
    }
  });

  test('Reverse', () {
    var l = [6, 5, 4, 3, 2, 1];
    reverse(l, 2, 4);
    expect(l, equals([6, 5, 3, 4, 2, 1]));
    reverse(l, 1, 1);
    expect(l, equals([6, 5, 3, 4, 2, 1]));
    reverse(l, 4, 6);
    expect(l, equals([6, 5, 3, 4, 1, 2]));
    reverse(l, 0, 2);
    expect(l, equals([5, 6, 3, 4, 1, 2]));
    reverse(l, 0, 6);
    expect(l, equals([2, 1, 4, 3, 6, 5]));
  });
}

class C {
  final int id;
  C(this.id);
}

int compareC(C one, C other) => one.id - other.id;
int cId(C c) => c.id;

/// Class naturally ordered by its first constructor argument.
class OC implements Comparable<OC> {
  final int id;
  final int order;
  OC(this.id, this.order);

  @override
  int compareTo(OC other) => id - other.id;

  @override
  String toString() => 'OC[$id,$order]';
}

int ocId(OC oc) => oc.id;
int ocOrder(OC oc) => oc.order;

int compareInt(int a, int b) => a - b;

/// Check that a list is sorted according to [compare] of [keyOf] of elements.
void expectSorted<T, K>(
    List<T> list, K Function(T element) keyOf, int Function(K a, K b) compare,
    [int start = 0, int? end]) {
  end ??= list.length;
  if (start == end) return;
  var prev = keyOf(list[start]);
  for (var i = start + 1; i < end; i++) {
    var next = keyOf(list[i]);
    expect(compare(prev, next), isNonPositive);
    prev = next;
  }
}
