// Copyright (c) 2018, 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 'dart:collection';

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

import 'utils/utils.dart';

void main() {
  group('without secondarySort', () {
    group('topologically sorts a graph', () {
      test('with no nodes', () {
        expect(_topologicalSort({}), isEmpty);
      });

      test('with only one node', () {
        expect(_topologicalSort({1: []}), equals([1]));
      });

      test('with no edges', () {
        expect(
          _topologicalSort({1: [], 2: [], 3: [], 4: []}),
          unorderedEquals([1, 2, 3, 4]),
        );
      });

      test('with single edges', () {
        expect(
          _topologicalSort({
            1: [2],
            2: [3],
            3: [4],
            4: [],
          }),
          equals([1, 2, 3, 4]),
        );
      });

      test('with many edges from one node', () {
        final result = _topologicalSort({
          1: [2, 3, 4],
          2: [],
          3: [],
          4: [],
        });
        expect(result.indexOf(1), lessThan(result.indexOf(2)));
        expect(result.indexOf(1), lessThan(result.indexOf(3)));
        expect(result.indexOf(1), lessThan(result.indexOf(4)));
      });

      test('with transitive edges', () {
        final result = _topologicalSort({
          1: [2, 4],
          2: [],
          3: [],
          4: [3],
        });
        expect(result.indexOf(1), lessThan(result.indexOf(2)));
        expect(result.indexOf(1), lessThan(result.indexOf(3)));
        expect(result.indexOf(1), lessThan(result.indexOf(4)));
        expect(result.indexOf(4), lessThan(result.indexOf(3)));
      });

      test('with diamond edges', () {
        final result = _topologicalSort({
          1: [2, 3],
          2: [4],
          3: [4],
          4: [],
        });
        expect(result.indexOf(1), lessThan(result.indexOf(2)));
        expect(result.indexOf(1), lessThan(result.indexOf(3)));
        expect(result.indexOf(1), lessThan(result.indexOf(4)));
        expect(result.indexOf(2), lessThan(result.indexOf(4)));
        expect(result.indexOf(3), lessThan(result.indexOf(4)));
      });
    });

    test('respects custom equality and hash functions', () {
      expect(
        _topologicalSort<int>(
          {
            0: [2],
            3: [4],
            5: [6],
            7: [],
          },
          equals: (i, j) => (i ~/ 2) == (j ~/ 2),
          hashCode: (i) => (i ~/ 2).hashCode,
        ),
        equals([
          0,
          anyOf([2, 3]),
          anyOf([4, 5]),
          anyOf([6, 7]),
        ]),
      );
    });

    group('throws a CycleException for a graph with', () {
      test('a one-node cycle', () {
        expect(
          () => _topologicalSort({
            1: [1],
          }),
          throwsCycleException([1]),
        );
      });

      test('a multi-node cycle', () {
        expect(
          () => _topologicalSort({
            1: [2],
            2: [3],
            3: [4],
            4: [1],
          }),
          throwsCycleException([4, 1, 2, 3]),
        );
      });
    });
  });

  group('with secondarySort', () {
    group('topologically sorts a graph', () {
      test('with no nodes', () {
        expect(_topologicalSort({}, secondarySort: true), isEmpty);
      });

      test('with only one node', () {
        expect(_topologicalSort({1: []}, secondarySort: true), equals([1]));
      });

      test('with no edges', () {
        expect(
          _topologicalSort({1: [], 2: [], 3: [], 4: []}, secondarySort: true),
          unorderedEquals([1, 2, 3, 4]),
        );
      });

      test('with single edges', () {
        expect(
          _topologicalSort(
            {
              1: [2],
              2: [3],
              3: [4],
              4: [],
            },
            secondarySort: true,
          ),
          equals([1, 2, 3, 4]),
        );
      });

      test('with many edges from one node', () {
        final result = _topologicalSort(
          {
            1: [2, 3, 4],
            2: [],
            3: [],
            4: [],
          },
          secondarySort: true,
        );
        expect(result.indexOf(1), lessThan(result.indexOf(2)));
        expect(result.indexOf(1), lessThan(result.indexOf(3)));
        expect(result.indexOf(1), lessThan(result.indexOf(4)));
      });

      test('with transitive edges', () {
        final result = _topologicalSort(
          {
            1: [2, 4],
            2: [],
            3: [],
            4: [3],
          },
          secondarySort: true,
        );
        expect(result.indexOf(1), lessThan(result.indexOf(2)));
        expect(result.indexOf(1), lessThan(result.indexOf(3)));
        expect(result.indexOf(1), lessThan(result.indexOf(4)));
        expect(result.indexOf(4), lessThan(result.indexOf(3)));
      });

      test('with diamond edges', () {
        final result = _topologicalSort(
          {
            1: [2, 3],
            2: [4],
            3: [4],
            4: [],
          },
          secondarySort: true,
        );
        expect(result.indexOf(1), lessThan(result.indexOf(2)));
        expect(result.indexOf(1), lessThan(result.indexOf(3)));
        expect(result.indexOf(1), lessThan(result.indexOf(4)));
        expect(result.indexOf(2), lessThan(result.indexOf(4)));
        expect(result.indexOf(3), lessThan(result.indexOf(4)));
      });
    });

    group('lexically sorts a graph where possible', () {
      test('with no edges', () {
        final result =
            _topologicalSort({4: [], 3: [], 1: [], 2: []}, secondarySort: true);
        expect(result, equals([1, 2, 3, 4]));
      });

      test('with one non-lexical edge', () {
        final result = _topologicalSort(
          {
            4: [],
            3: [1],
            1: [],
            2: [],
          },
          secondarySort: true,
        );
        expect(
          result,
          equals(
            anyOf([
              [2, 3, 1, 4],
              [3, 1, 2, 4],
            ]),
          ),
        );
      });

      test('with a non-lexical topolgical order', () {
        final result = _topologicalSort(
          {
            4: [3],
            3: [2],
            2: [1],
            1: [],
          },
          secondarySort: true,
        );
        expect(result, equals([4, 3, 2, 1]));
      });

      group('with multiple layers', () {
        test('in lexical order', () {
          final result = _topologicalSort(
            {
              1: [2],
              2: [3],
              3: [],
              4: [5],
              5: [6],
              6: [],
            },
            secondarySort: true,
          );
          expect(result, equals([1, 2, 3, 4, 5, 6]));
        });

        test('in non-lexical order', () {
          final result = _topologicalSort(
            {
              1: [3],
              3: [5],
              4: [2],
              2: [6],
              5: [],
              6: [],
            },
            secondarySort: true,
          );
          expect(
            result,
            anyOf([
              equals([1, 3, 4, 2, 5, 6]),
              equals([1, 4, 2, 3, 5, 6]),
            ]),
          );
        });
      });
    });

    test('respects custom equality and hash functions', () {
      expect(
        _topologicalSort<int>(
          {
            0: [2],
            3: [4],
            5: [6],
            7: [],
          },
          equals: (i, j) => (i ~/ 2) == (j ~/ 2),
          hashCode: (i) => (i ~/ 2).hashCode,
          secondarySort: true,
        ),
        equals([
          0,
          anyOf([2, 3]),
          anyOf([4, 5]),
          anyOf([6, 7]),
        ]),
      );
    });

    group('throws a CycleException for a graph with', () {
      test('a one-node cycle', () {
        expect(
          () => _topologicalSort(
            {
              1: [1],
            },
            secondarySort: true,
          ),
          throwsCycleException([1]),
        );
      });

      test('a multi-node cycle', () {
        expect(
          () => _topologicalSort(
            {
              1: [2],
              2: [3],
              3: [4],
              4: [1],
            },
            secondarySort: true,
          ),
          throwsCycleException([4, 1, 2, 3]),
        );
      });
    });
  });
}

/// Runs a topological sort on a graph represented a map from keys to edges.
List<T> _topologicalSort<T>(
  Map<T, List<T>> graph, {
  bool Function(T, T)? equals,
  int Function(T)? hashCode,
  bool secondarySort = false,
}) {
  if (equals != null) {
    graph = LinkedHashMap(equals: equals, hashCode: hashCode)..addAll(graph);
  }
  return topologicalSort(
    graph.keys,
    (node) {
      expect(graph, contains(node));
      return graph[node]!;
    },
    equals: equals,
    hashCode: hashCode,
    secondarySort:
        secondarySort ? (a, b) => (a as Comparable<T>).compareTo(b) : null,
  );
}
