// Copyright (c) 2012, 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' show pumpEventQueue;

import '../description.dart';
import '../interfaces.dart';
import '../util.dart';
import 'async_matcher.dart';
import 'expect.dart';
import 'throws_matcher.dart';
import 'util/pretty_print.dart';

/// Matches a [Future] that completes successfully with any value.
///
/// This creates an asynchronous expectation. The call to [expect] will return
/// immediately and execution will continue. To wait for the future to
/// complete and the expectation to run use [expectLater] and wait on the
/// returned future.
///
/// To test that a Future completes with an exception, you can use [throws] and
/// [throwsA].
final Matcher completes = const _Completes(null);

/// Matches a [Future] that completes successfully with a value that matches
/// [matcher].
///
/// This creates an asynchronous expectation. The call to [expect] will return
/// immediately and execution will continue. Later, when the future completes,
/// the expectation against [matcher] will run. To wait for the future to
/// complete and the expectation to run use [expectLater] and wait on the
/// returned future.
///
/// To test that a Future completes with an exception, you can use [throws] and
/// [throwsA].
Matcher completion(
  Object? matcher, [
  @Deprecated('this parameter is ignored') String? description,
]) => _Completes(wrapMatcher(matcher));

class _Completes extends AsyncMatcher {
  final Matcher? _matcher;

  const _Completes(this._matcher);

  // Avoid async/await so we synchronously start listening to [item].
  @override
  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
    if (item is! Future) return 'was not a Future';

    return item.then((value) async {
      if (_matcher == null) return null;

      String? result;
      if (_matcher is AsyncMatcher) {
        result = await _matcher.matchAsync(value) as String?;
        if (result == null) return null;
      } else {
        var matchState = <Object?, Object?>{};
        if (_matcher.matches(value, matchState)) return null;
        result =
            _matcher
                .describeMismatch(value, StringDescription(), matchState, false)
                .toString();
      }

      var buffer = StringBuffer();
      buffer.writeln(indent(prettyPrint(value), first: 'emitted '));
      if (result.isNotEmpty) buffer.writeln(indent(result, first: '  which '));
      return buffer.toString().trimRight();
    });
  }

  @override
  Description describe(Description description) {
    if (_matcher == null) {
      description.add('completes successfully');
    } else {
      description.add('completes to a value that ').addDescriptionOf(_matcher);
    }
    return description;
  }
}

/// Matches a [Future] that does not complete.
///
/// Note that this creates an asynchronous expectation. The call to
/// `expect()` that includes this will return immediately and execution will
/// continue.
final Matcher doesNotComplete = const _DoesNotComplete();

class _DoesNotComplete extends Matcher {
  const _DoesNotComplete();

  @override
  Description describe(Description description) {
    description.add('does not complete');
    return description;
  }

  @override
  bool matches(Object? item, Map matchState) {
    if (item is! Future) return false;
    item.then((value) {
      fail(
        'Future was not expected to complete but completed with a value of '
        '$value',
      );
    });
    expect(pumpEventQueue(), completes);
    return true;
  }

  @override
  Description describeMismatch(
    Object? item,
    Description description,
    Map matchState,
    bool verbose,
  ) {
    if (item is! Future) return description.add('$item is not a Future');
    return description;
  }
}
