// 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 'dart:async';
import 'dart:collection';

import 'package:boolean_selector/boolean_selector.dart';
import 'package:glob/glob.dart';
import 'package:test/test.dart';
import 'package:test_api/src/backend/declarer.dart';
import 'package:test_api/src/backend/group.dart';
import 'package:test_api/src/backend/group_entry.dart';
import 'package:test_api/src/backend/live_test.dart';
import 'package:test_api/src/backend/platform_selector.dart';
import 'package:test_api/src/backend/runtime.dart';
import 'package:test_api/src/backend/state.dart';
import 'package:test_api/src/backend/suite_platform.dart';
import 'package:test_core/src/runner/application_exception.dart';
import 'package:test_core/src/runner/compiler_selection.dart';
import 'package:test_core/src/runner/configuration.dart';
import 'package:test_core/src/runner/configuration/custom_runtime.dart';
import 'package:test_core/src/runner/configuration/runtime_settings.dart';
import 'package:test_core/src/runner/engine.dart';
import 'package:test_core/src/runner/load_suite.dart';
import 'package:test_core/src/runner/plugin/environment.dart';
import 'package:test_core/src/runner/runner_suite.dart';
import 'package:test_core/src/runner/runtime_selection.dart';
import 'package:test_core/src/runner/suite.dart';

/// A dummy suite platform to use for testing suites.
final suitePlatform = SuitePlatform(Runtime.vm, compiler: null);

// The last state change detected via [expectStates].
State? _lastState;

/// Asserts that exactly [states] will be emitted via [liveTest.onStateChange].
///
/// The most recent emitted state is stored in [_lastState].
void expectStates(LiveTest liveTest, Iterable<State> statesIter) {
  var states = Queue.of(statesIter);
  liveTest.onStateChange.listen(
    expectAsync1(
      (state) {
        _lastState = state;
        expect(state, equals(states.removeFirst()));
      },
      count: states.length,
      max: states.length,
    ),
  );
}

/// Asserts that errors will be emitted via [liveTest.onError] that match
/// [validators], in order.
void expectErrors(
  LiveTest liveTest,
  Iterable<void Function(Object)> validatorsIter,
) {
  var validators = Queue.of(validatorsIter);
  liveTest.onError.listen(
    expectAsync1(
      (error) {
        validators.removeFirst()(error.error);
      },
      count: validators.length,
      max: validators.length,
    ),
  );
}

/// Asserts that [liveTest] will have a single failure with message `"oh no"`.
void expectSingleFailure(LiveTest liveTest) {
  expectStates(liveTest, [
    const State(Status.running, Result.success),
    const State(Status.complete, Result.failure),
  ]);

  expectErrors(liveTest, [
    (error) {
      expect(_lastState!.status, equals(Status.complete));
      expect(error, _isTestFailure('oh no'));
    },
  ]);
}

/// Returns a matcher that matches a [TestFailure] with the given [message].
///
/// [message] can be a string or a [Matcher].
Matcher _isTestFailure(Object message) => const TypeMatcher<TestFailure>()
    .having((e) => e.message, 'message', message);

/// Returns a matcher that matches a [ApplicationException] with the given
/// [message].
///
/// [message] can be a string or a [Matcher].
Matcher isApplicationException(Object message) =>
    const TypeMatcher<ApplicationException>().having(
      (e) => e.message,
      'message',
      message,
    );

/// Asserts that [liveTest] has completed and passed.
///
/// If the test had any errors, they're surfaced nicely into the outer test.
void expectTestPassed(LiveTest liveTest) {
  // Since the test is expected to pass, we forward any current or future errors
  // to the outer test, because they're definitely unexpected.
  for (var error in liveTest.errors) {
    registerException(error.error, error.stackTrace);
  }
  liveTest.onError.listen((error) {
    registerException(error.error, error.stackTrace);
  });

  expect(liveTest.state.status, equals(Status.complete));
  expect(liveTest.state.result, equals(Result.success));
}

/// Asserts that [liveTest] failed with a single [TestFailure] whose message
/// matches [message].
void expectTestFailed(LiveTest liveTest, Object message) {
  expect(liveTest.state.status, equals(Status.complete));
  expect(liveTest.state.result, equals(Result.failure));
  expect(liveTest.errors, hasLength(1));
  expect(liveTest.errors.first.error, _isTestFailure(message));
}

/// Runs [body] with a declarer and returns the declared entries.
List<GroupEntry> declare(void Function() body) {
  var declarer = Declarer()..declare(body);
  return declarer.build().entries;
}

/// Runs [body] with a declarer and returns an engine that runs those tests.
Engine declareEngine(
  void Function() body, {
  bool runSkipped = false,
  String? coverage,
  String? coverageLcov,
  bool stopOnFirstFailure = false,
}) {
  var declarer = Declarer()..declare(body);
  return Engine.withSuites(
    [
      RunnerSuite(
        const PluginEnvironment(),
        SuiteConfiguration.runSkipped(runSkipped),
        declarer.build(),
        suitePlatform,
      ),
    ],
    coverage: coverage,
    coverageLcov: coverageLcov,
    stopOnFirstFailure: stopOnFirstFailure,
  );
}

/// Returns a [RunnerSuite] with a default environment and configuration.
RunnerSuite runnerSuite(Group root) => RunnerSuite(
  const PluginEnvironment(),
  SuiteConfiguration.empty,
  root,
  suitePlatform,
);

/// Returns a [LoadSuite] with a default configuration.
LoadSuite loadSuite(String name, FutureOr<RunnerSuite> Function() body) =>
    LoadSuite(name, SuiteConfiguration.empty, suitePlatform, body);

SuiteConfiguration suiteConfiguration({
  bool? allowDuplicateTestNames,
  bool? allowTestRandomization,
  bool? jsTrace,
  bool? runSkipped,
  Iterable<String>? dart2jsArgs,
  String? precompiledPath,
  Iterable<CompilerSelection>? compilerSelections,
  Iterable<RuntimeSelection>? runtimes,
  Map<BooleanSelector, SuiteConfiguration>? tags,
  Map<PlatformSelector, SuiteConfiguration>? onPlatform,
  bool? ignoreTimeouts,

  // Test-level configuration
  Timeout? timeout,
  bool? verboseTrace,
  bool? chainStackTraces,
  bool? skip,
  int? retry,
  String? skipReason,
  PlatformSelector? testOn,
  Iterable<String>? addTags,
}) => SuiteConfiguration(
  allowDuplicateTestNames: allowDuplicateTestNames,
  allowTestRandomization: allowTestRandomization,
  jsTrace: jsTrace,
  runSkipped: runSkipped,
  dart2jsArgs: dart2jsArgs,
  precompiledPath: precompiledPath,
  compilerSelections: compilerSelections,
  runtimes: runtimes,
  tags: tags,
  onPlatform: onPlatform,
  ignoreTimeouts: ignoreTimeouts,
  timeout: timeout,
  verboseTrace: verboseTrace,
  chainStackTraces: chainStackTraces,
  skip: skip,
  retry: retry,
  skipReason: skipReason,
  testOn: testOn,
  addTags: addTags,
);

Configuration configuration({
  bool? help,
  String? customHtmlTemplatePath,
  bool? version,
  bool? pauseAfterLoad,
  bool? debug,
  bool? color,
  String? configurationPath,
  String? reporter,
  Map<String, String>? fileReporters,
  String? coverage,
  String? coverageLcov,
  bool? branchCoverage,
  List<RegExp>? coveragePackages,
  int? concurrency,
  int? shardIndex,
  int? totalShards,
  Map<String, Set<TestSelection>>? testSelections,
  Iterable<String>? foldTraceExcept,
  Iterable<String>? foldTraceOnly,
  Glob? filename,
  Iterable<String>? chosenPresets,
  Map<String, Configuration>? presets,
  Map<String, RuntimeSettings>? overrideRuntimes,
  Map<String, CustomRuntime>? defineRuntimes,
  bool? noRetry,
  bool? ignoreTimeouts,

  // Suite-level configuration
  bool? allowDuplicateTestNames,
  bool? allowTestRandomization,
  bool? jsTrace,
  bool? runSkipped,
  Iterable<String>? dart2jsArgs,
  String? precompiledPath,
  Iterable<Pattern>? globalPatterns,
  Iterable<CompilerSelection>? compilerSelections,
  Iterable<RuntimeSelection>? runtimes,
  BooleanSelector? includeTags,
  BooleanSelector? excludeTags,
  Map<BooleanSelector, SuiteConfiguration>? tags,
  Map<PlatformSelector, SuiteConfiguration>? onPlatform,
  int? testRandomizeOrderingSeed,

  // Test-level configuration
  Timeout? timeout,
  bool? verboseTrace,
  bool? chainStackTraces,
  bool? skip,
  int? retry,
  String? skipReason,
  PlatformSelector? testOn,
  Iterable<String>? addTags,
}) => Configuration(
  help: help,
  customHtmlTemplatePath: customHtmlTemplatePath,
  version: version,
  pauseAfterLoad: pauseAfterLoad,
  debug: debug,
  color: color,
  configurationPath: configurationPath,
  reporter: reporter,
  fileReporters: fileReporters,
  coverage: coverage,
  coverageLcov: coverageLcov,
  branchCoverage: branchCoverage,
  coveragePackages: coveragePackages,
  concurrency: concurrency,
  shardIndex: shardIndex,
  totalShards: totalShards,
  testSelections: testSelections,
  foldTraceExcept: foldTraceExcept,
  foldTraceOnly: foldTraceOnly,
  filename: filename,
  chosenPresets: chosenPresets,
  presets: presets,
  overrideRuntimes: overrideRuntimes,
  defineRuntimes: defineRuntimes,
  noRetry: noRetry,
  ignoreTimeouts: ignoreTimeouts,
  allowDuplicateTestNames: allowDuplicateTestNames,
  allowTestRandomization: allowTestRandomization,
  jsTrace: jsTrace,
  runSkipped: runSkipped,
  dart2jsArgs: dart2jsArgs,
  precompiledPath: precompiledPath,
  globalPatterns: globalPatterns,
  compilerSelections: compilerSelections,
  runtimes: runtimes,
  includeTags: includeTags,
  excludeTags: excludeTags,
  tags: tags,
  onPlatform: onPlatform,
  testRandomizeOrderingSeed: testRandomizeOrderingSeed,
  stopOnFirstFailure: false,
  timeout: timeout,
  verboseTrace: verboseTrace,
  chainStackTraces: chainStackTraces,
  skip: skip,
  retry: retry,
  skipReason: skipReason,
  testOn: testOn,
  addTags: addTags,
);
