// Copyright (c) 2015, 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:stack_trace/stack_trace.dart';

import 'group_entry.dart';
import 'metadata.dart';
import 'suite_platform.dart';
import 'test.dart';
import 'test_location.dart';

/// A group contains one or more tests and subgroups.
///
/// It includes metadata that applies to all contained tests.
class Group implements GroupEntry {
  @override
  final String name;

  @override
  Group? parent;

  @override
  final Metadata metadata;

  @override
  final Trace? trace;

  @override
  final TestLocation? location;

  /// The children of this group.
  final List<GroupEntry> entries;

  /// Returns a new root-level group.
  Group.root(Iterable<GroupEntry> entries, {Metadata? metadata})
      : this('', entries, metadata: metadata);

  /// A test to run before all tests in the group.
  ///
  /// This is `null` if no `setUpAll` callbacks were declared.
  final Test? setUpAll;

  /// A test to run after all tests in the group.
  ///
  /// This is `null` if no `tearDown` callbacks were declared.
  final Test? tearDownAll;

  /// The number of tests (recursively) in this group.
  int get testCount {
    if (_testCount != null) return _testCount!;
    _testCount = entries.fold<int>(
        0, (count, entry) => count + (entry is Group ? entry.testCount : 1));
    return _testCount!;
  }

  int? _testCount;

  Group(this.name, Iterable<GroupEntry> entries,
      {Metadata? metadata,
      this.trace,
      this.location,
      this.setUpAll,
      this.tearDownAll})
      : entries = List<GroupEntry>.unmodifiable(entries),
        metadata = metadata ?? Metadata() {
    for (var entry in entries) {
      assert(entry.parent == null);
      entry.parent = this;
    }
    assert(setUpAll?.parent == null);
    setUpAll?.parent = this;
    assert(tearDownAll?.parent == null);
    tearDownAll?.parent = this;
  }

  @override
  Group? forPlatform(SuitePlatform platform) {
    if (!metadata.testOn.evaluate(platform)) return null;
    var newMetadata = metadata.forPlatform(platform);
    var filtered = _map((entry) => entry.forPlatform(platform));
    if (filtered.isEmpty && entries.isNotEmpty) return null;
    return Group(name, filtered,
        metadata: newMetadata,
        trace: trace,
        location: location,
        setUpAll: setUpAll?.forPlatform(platform),
        tearDownAll: tearDownAll?.forPlatform(platform));
  }

  @override
  Group? filter(bool Function(Test) callback) {
    var filtered = _map((entry) => entry.filter(callback));
    if (filtered.isEmpty && entries.isNotEmpty) return null;
    return Group(name, filtered,
        metadata: metadata,
        trace: trace,
        location: location,
        // Always clone these because they are being re-parented.
        setUpAll: setUpAll?.clone(),
        tearDownAll: tearDownAll?.clone());
  }

  @override
  Group? clone() {
    var entries = _map((entry) => entry.clone());
    return Group(name, entries,
        metadata: metadata,
        trace: trace,
        location: location,
        // Always clone these because they are being re-parented.
        setUpAll: setUpAll?.clone(),
        tearDownAll: tearDownAll?.clone());
  }

  /// Returns the entries of this group mapped using [callback].
  ///
  /// Any `null` values returned by [callback] will be removed.
  List<GroupEntry> _map(GroupEntry? Function(GroupEntry) callback) {
    return entries
        .map((entry) => callback(entry))
        .whereType<GroupEntry>()
        .toList();
  }
}
