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

import 'package:path/path.dart' as p;
import 'package:test_api/backend.dart';
import 'package:test_api/src/backend/declarer.dart'; //ignore: implementation_imports
import 'package:test_api/src/backend/group.dart'; //ignore: implementation_imports
import 'package:test_api/src/backend/group_entry.dart'; //ignore: implementation_imports
import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/test.dart'; //ignore: implementation_imports

import 'runner/configuration.dart';
import 'runner/engine.dart';
import 'runner/plugin/environment.dart';
import 'runner/reporter.dart';
import 'runner/reporter/expanded.dart';
import 'runner/runner_suite.dart';
import 'runner/suite.dart';
import 'util/os.dart';
import 'util/print_sink.dart';

/// Runs all unskipped test cases declared in [testMain].
///
/// Test suite level metadata defined in annotations is not read. No filtering
/// is applied except for the filtering defined by `solo` or `skip` arguments to
/// `group` and `test`. Returns [true] if all tests passed.
Future<bool> directRunTests(
  FutureOr<void> Function() testMain, {
  Reporter Function(Engine)? reporterFactory,
  // TODO: Change the default https://github.com/dart-lang/test/issues/1571
  bool allowDuplicateTestNames = true,
}) => _directRunTests(
  testMain,
  reporterFactory: reporterFactory,
  allowDuplicateTestNames: allowDuplicateTestNames,
);

/// Runs a single test declared in [testMain] matched by it's full test name.
///
/// There must be exactly one test defined with the name [fullTestName]. Note
/// that not all tests and groups are checked, so a test case that is not be
/// intended to be run (due to a `solo` on a different test) may still be run
/// with this API. Only the test names returned by [enumerateTestCases] should
/// be used to prevent running skipped tests.
///
/// Return [true] if the test passes.
///
/// If there are no tests matching [fullTestName] a [MissingTestException] is
/// thrown. If there is more than one test with the name [fullTestName] they
/// will both be run, then a [DuplicateTestnameException] will be thrown.
Future<bool> directRunSingleTest(
  FutureOr<void> Function() testMain,
  String fullTestName, {
  Reporter Function(Engine)? reporterFactory,
}) => _directRunTests(
  testMain,
  reporterFactory: reporterFactory,
  fullTestName: fullTestName,
  allowDuplicateTestNames: false,
);

Future<bool> _directRunTests(
  FutureOr<void> Function() testMain, {
  Reporter Function(Engine)? reporterFactory,
  String? fullTestName,
  required bool allowDuplicateTestNames,
}) async {
  reporterFactory ??=
      (engine) => ExpandedReporter.watch(
        engine,
        PrintSink(),
        color: Configuration.empty.color,
        printPath: false,
        printPlatform: false,
      );
  final declarer = Declarer(
    fullTestName: fullTestName,
    allowDuplicateTestNames: allowDuplicateTestNames,
  );
  await declarer.declare(testMain);

  final suite = RunnerSuite(
    const PluginEnvironment(),
    SuiteConfiguration.empty,
    declarer.build(),
    SuitePlatform(Runtime.vm, compiler: null, os: currentOSGuess),
    path: p.prettyUri(Uri.base),
  );

  final engine =
      Engine()
        ..suiteSink.add(suite)
        ..suiteSink.close();

  reporterFactory(engine);

  final success =
      await runZoned(
        () => Invoker.guard(engine.run),
        zoneValues: {#test.declarer: declarer},
      ) ??
      false;

  if (fullTestName != null) {
    final testCount = engine.liveTests.length;
    if (testCount == 0) {
      throw MissingTestException(fullTestName);
    }
  }
  return success;
}

/// Runs [testMain] and returns the names of all declared tests.
///
/// Test names declared must be unique. If any test repeats the full name,
/// including group prefixes, of a prior test a [DuplicateTestNameException]
/// will be thrown.
///
/// Skipped tests are ignored.
Future<Set<String>> enumerateTestCases(
  FutureOr<void> Function() testMain,
) async {
  final declarer = Declarer();
  await declarer.declare(testMain);

  final toVisit = Queue<GroupEntry>.of([declarer.build()]);
  final unskippedTestNames = <String>{};
  while (toVisit.isNotEmpty) {
    final current = toVisit.removeLast();
    if (current is Group) {
      toVisit.addAll(current.entries.reversed);
    } else if (current is Test) {
      if (current.metadata.skip) continue;
      unskippedTestNames.add(current.name);
    } else {
      throw StateError('Unhandled Group Entry: ${current.runtimeType}');
    }
  }
  return unskippedTestNames;
}

/// An exception thrown when a specific test was requested by name that does not
/// exist.
class MissingTestException implements Exception {
  final String name;
  MissingTestException(this.name);

  @override
  String toString() =>
      'A test with the name "$name" was not declared in the test suite.';
}
