// Copyright (c) 2014, 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 '../interfaces.dart';
import '../util.dart';
import 'async_matcher.dart';
import 'util/pretty_print.dart';

/// This function is deprecated.
///
/// Use [throwsA] instead. We strongly recommend that you add assertions about
/// at least the type of the error, but you can write `throwsA(anything)` to
/// mimic the behavior of this matcher.
@Deprecated('Will be removed in 0.13.0')
const Matcher throws = Throws();

/// This can be used to match three kinds of objects:
///
/// * A [Function] that throws an exception when called. The function cannot
///   take any arguments. If you want to test that a function expecting
///   arguments throws, wrap it in another zero-argument function that calls
///   the one you want to test.
///
/// * A [Future] that completes with an exception. Note that this creates an
///   asynchronous expectation. The call to `expect()` that includes this will
///   return immediately and execution will continue. Later, when the future
///   completes, the actual expectation will run.
///
/// * A [Function] that returns a [Future] that completes with an exception.
///
/// In all three cases, when an exception is thrown, this will test that the
/// exception object matches [matcher]. If [matcher] is not an instance of
/// [Matcher], it will implicitly be treated as `equals(matcher)`.
///
/// Examples:
/// ```dart
/// void functionThatThrows() => throw SomeException();
///
/// void functionWithArgument(bool shouldThrow) {
///   if (shouldThrow) {
///     throw SomeException();
///   }
/// }
///
/// Future<void> asyncFunctionThatThrows() async => throw SomeException();
///
/// expect(functionThatThrows, throwsA(isA<SomeException>()));
///
/// expect(() => functionWithArgument(true), throwsA(isA<SomeException>()));
///
/// var future = asyncFunctionThatThrows();
/// await expectLater(future, throwsA(isA<SomeException>()));
///
/// await expectLater(
///     asyncFunctionThatThrows, throwsA(isA<SomeException>()));
/// ```
Matcher throwsA(Object? matcher) => Throws(wrapMatcher(matcher));

/// Use the [throwsA] function instead.
@Deprecated('Will be removed in 0.13.0')
class Throws extends AsyncMatcher {
  final Matcher? _matcher;

  const Throws([Matcher? matcher]) : _matcher = matcher;

  // Avoid async/await so we synchronously fail if we match a synchronous
  // function.
  @override
  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
    if (item is! Function && item is! Future) {
      return 'was not a Function or Future';
    }

    if (item is Future) {
      return _matchFuture(item, 'emitted ');
    }

    try {
      var value = (item as Function)();
      if (value is Future) {
        return _matchFuture(value, 'returned a Future that emitted ');
      }

      return indent(prettyPrint(value), first: 'returned ');
    } catch (error, trace) {
      return _check(error, trace);
    }
  }

  /// Matches [future], using try/catch since `onError` doesn't seem to work
  /// properly in nnbd.
  Future<String?> _matchFuture(
    Future<dynamic> future,
    String messagePrefix,
  ) async {
    try {
      var value = await future;
      return indent(prettyPrint(value), first: messagePrefix);
    } catch (error, trace) {
      return _check(error, trace);
    }
  }

  @override
  Description describe(Description description) {
    if (_matcher == null) {
      return description.add('throws');
    } else {
      return description.add('throws ').addDescriptionOf(_matcher);
    }
  }

  /// Verifies that [error] matches [_matcher] and returns a [String]
  /// description of the failure if it doesn't.
  String? _check(Object error, StackTrace? trace) {
    if (_matcher == null) return null;

    var matchState = <Object?, Object?>{};
    if (_matcher.matches(error, matchState)) return null;

    var result =
        _matcher
            .describeMismatch(error, StringDescription(), matchState, false)
            .toString();

    var buffer = StringBuffer();
    buffer.writeln(indent(prettyPrint(error), first: 'threw '));
    if (trace != null) {
      buffer.writeln(
        indent(
          TestHandle.current.formatStackTrace(trace).toString(),
          first: 'stack ',
        ),
      );
    }
    if (result.isNotEmpty) buffer.writeln(indent(result, first: 'which '));
    return buffer.toString().trimRight();
  }
}
