// Copyright (c) 2023, 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 'src/backend/group.dart';
import 'src/backend/invoker.dart';
import 'src/backend/live_test.dart';
import 'src/backend/metadata.dart';
import 'src/backend/runtime.dart';
import 'src/backend/state.dart';
import 'src/backend/suite.dart';
import 'src/backend/suite_platform.dart';

@Deprecated('These classes are unused for monitoring tests. Use `State`.')
export 'src/backend/state.dart' show Result, Status;
export 'src/backend/test_failure.dart' show TestFailure;

/// A monitor for the behavior of a callback when it is run as the body of a
/// test case.
///
/// Allows running a callback as the body of a local test case and querying for
/// the current [state], and [errors], and subscribing to future errors.
///
/// Use [run] to run a test body and query for the success or failure.
///
/// Use [start] to start a test and query for whether it has finished running.
final class TestCaseMonitor {
  final LiveTest _liveTest;
  final _done = Completer<void>();
  TestCaseMonitor._(FutureOr<void> Function() body)
    : _liveTest = _createTest(body);

  /// Run [body] as a test case and return a [TestCaseMonitor] with the result.
  ///
  /// The [state] will either [State.passed], [State.skipped], or
  /// [State.failed], the test will no longer be running.
  ///
  /// {@template result-late-fail}
  /// Note that a test can change state from [State.passed] to [State.failed]
  /// if the test surfaces an unawaited asynchronous error.
  /// {@endtemplate}
  ///
  /// ```dart
  /// final monitor = await TestCaseMonitor.run(() {
  ///   fail('oh no!');
  /// });
  /// assert(monitor.state == State.failed);
  /// assert((monitor.errors.single.error as TestFailure).message == 'oh no!');
  /// ```
  static Future<TestCaseMonitor> run(FutureOr<void> Function() body) async {
    final monitor = TestCaseMonitor.start(body);
    await monitor.onDone;
    return monitor;
  }

  /// Start [body] as a test case and return a [TestCaseMonitor] with the status
  /// and result.
  ///
  /// The [state] will start as [State.pending] if queried synchronously, but it
  /// will switch to [State.running]. After `onDone` completes the state will be
  /// one of [State.passed], [State.skipped], or [State.failed].
  ///
  /// {@macro result-late-fail}
  ///
  /// ```dart
  /// late void Function() completeWork;
  /// final monitor = TestCaseMonitor.start(() {
  ///   final outstandingWork = TestHandle.current.markPending();
  ///   completeWork = outstandingWork.complete;
  /// });
  /// await pumpEventQueue();
  /// assert(monitor.state == State.running);
  /// completeWork();
  /// await monitor.onDone;
  /// assert(monitor.state == State.passed);
  /// ```
  static TestCaseMonitor start(FutureOr<void> Function() body) =>
      TestCaseMonitor._(body).._start();

  void _start() {
    _liveTest.run().whenComplete(_done.complete);
  }

  /// A future that completes after this test has finished running, or has
  /// surfaced an error.
  Future<void> get onDone => _done.future;

  /// The running and success or failure status for the test case.
  State get state {
    final status = _liveTest.state.status;
    if (status == Status.pending) return State.pending;
    if (status == Status.running) return State.running;
    final result = _liveTest.state.result;
    if (result == Result.skipped) return State.skipped;
    if (result == Result.success) return State.passed;
    // result == Result.failure || result == Result.error
    return State.failed;
  }

  /// The errors surfaced by the test.
  ///
  /// A test with any errors will have a [state] of [State.failed].
  ///
  /// {@macro result-late-fail}
  ///
  /// A test may have more than one error if there were unhandled asynchronous
  /// errors surfaced after the test is done.
  Iterable<AsyncError> get errors => _liveTest.errors;

  /// A stream of errors surfaced by the test.
  ///
  /// This stream will not close, asynchronous errors may be surfaced within the
  /// test's error zone at any point.
  Stream<AsyncError> get onError => _liveTest.onError;
}

/// Returns a local [LiveTest] that runs [body].
LiveTest _createTest(FutureOr<void> Function() body) {
  var test = LocalTest('test', Metadata(chainStackTraces: true), body);
  var suite = Suite(Group.root([test]), _suitePlatform, ignoreTimeouts: false);
  return test.load(suite);
}

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

/// The running and success state of a test monitored by a [TestCaseMonitor].
enum State {
  /// The test is has not yet started.
  pending,

  /// The test is running and has not yet failed.
  running,

  /// The test has completed without any error.
  ///
  /// This implies that the test body has completed, and no error has surfaced
  /// *yet*. However, it this doesn't mean that the test won't fail in the
  /// future.
  passed,

  /// The test, or some part of it, has been skipped.
  ///
  /// This does not imply that the test has not had an error, but if there are
  /// errors they are ignored.
  skipped,

  /// The test has failed.
  ///
  /// An test fails when any exception, typically a [TestFailure], is thrown in
  /// the test's zone. A test that has failed may still have additional errors
  /// that surface as unhandled asynchronous errors.
  failed,
}
