// Copyright (c) 2017, 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.

// ignore_for_file: deprecated_member_use_from_same_package

import 'package:test_api/hooks.dart';

import '../description.dart';
import '../equals_matcher.dart';
import '../interfaces.dart';
import '../operator_matchers.dart';
import '../type_matcher.dart';
import 'expect.dart';

/// A matcher that does asynchronous computation.
///
/// Rather than implementing [matches], subclasses implement [matchAsync].
/// [AsyncMatcher.matches] ensures that the test doesn't complete until the
/// returned future completes, and [expect] returns a future that completes when
/// the returned future completes so that tests can wait for it.
abstract class AsyncMatcher extends Matcher {
  const AsyncMatcher();

  /// Returns `null` if this matches [item], or a [String] description of the
  /// failure if it doesn't match.
  ///
  /// This can return a [Future] or a synchronous value. If it returns a
  /// [Future], neither [expect] nor the test will complete until that [Future]
  /// completes.
  ///
  /// If this returns a [String] synchronously, [expect] will synchronously
  /// throw a [TestFailure] and [matches] will synchronously return `false`.
  dynamic /*FutureOr<String>*/ matchAsync(dynamic item);

  @override
  bool matches(dynamic item, Map matchState) {
    final result = matchAsync(item);
    expect(
      result,
      anyOf([
        equals(null),
        const TypeMatcher<Future>(),
        const TypeMatcher<String>(),
      ]),
      reason: 'matchAsync() may only return a String, a Future, or null.',
    );

    if (result is Future) {
      final outstandingWork = TestHandle.current.markPending();
      result.then((realResult) {
        if (realResult != null) {
          fail(formatFailure(this, item, realResult as String));
        }
        outstandingWork.complete();
      });
    } else if (result is String) {
      matchState[this] = result;
      return false;
    }

    return true;
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map matchState,
    bool verbose,
  ) => StringDescription(matchState[this] as String);
}
