// Copyright (c) 2018, 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 'package:meta/meta.dart';

import 'having_matcher.dart';
import 'interfaces.dart';

/// Returns a matcher that matches objects with type [T].
///
/// ```dart
/// expect(shouldBeDuration, isA<Duration>());
/// ```
///
/// Expectations can be chained on top of the type using the
/// [TypeMatcher.having] method to add additional constraints.
TypeMatcher<T> isA<T>() => TypeMatcher<T>();

/// A [Matcher] subclass that supports validating the [Type] of the target
/// object.
///
/// ```dart
/// expect(shouldBeDuration, TypeMatcher<Duration>());
/// ```
///
/// If you want to further validate attributes of the specified [Type], use the
/// [having] function.
///
/// ```dart
/// void shouldThrowRangeError(int value) {
///   throw RangeError.range(value, 10, 20);
/// }
///
/// expect(
///     () => shouldThrowRangeError(5),
///     throwsA(const TypeMatcher<RangeError>()
///         .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
///         .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
/// ```
///
/// Notice that you can chain multiple calls to [having] to verify multiple
/// aspects of an object.
///
/// Note: All of the top-level `isType` matchers exposed by this package are
/// instances of [TypeMatcher], so you can use the [having] function without
/// creating your own instance.
///
/// ```dart
/// expect(
///     () => shouldThrowRangeError(5),
///     throwsA(isRangeError
///         .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
///         .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
/// ```
class TypeMatcher<T> extends Matcher {
  final String? _name;

  /// Create a matcher matches instances of type [T].
  ///
  /// For a fluent API to create TypeMatchers see [isA].
  const TypeMatcher(
      [@Deprecated('Provide a type argument to TypeMatcher and omit the name. '
          'This argument will be removed in the next release.')
      String? name])
      : _name =
            // ignore: deprecated_member_use_from_same_package
            name;

  /// Returns a new [TypeMatcher] that validates the existing type as well as
  /// a specific [feature] of the object with the provided [matcher].
  ///
  /// Provides a human-readable [description] of the [feature] to make debugging
  /// failures easier.
  ///
  /// ```dart
  /// /// Validates that the object is a [RangeError] with a message containing
  /// /// the string 'details' and `start` and `end` properties that are `null`.
  /// final _rangeMatcher = isRangeError
  ///    .having((e) => e.message, 'message', contains('details'))
  ///    .having((e) => e.start, 'start', isNull)
  ///    .having((e) => e.end, 'end', isNull);
  /// ```
  @useResult
  TypeMatcher<T> having(
          Object? Function(T) feature, String description, dynamic matcher) =>
      HavingMatcher(this, description, feature, matcher);

  @override
  Description describe(Description description) {
    var name = _name ?? _stripDynamic(T);
    return description.add("<Instance of '$name'>");
  }

  @override
  bool matches(Object? item, Map matchState) => item is T;

  @override
  Description describeMismatch(dynamic item, Description mismatchDescription,
      Map matchState, bool verbose) {
    var name = _name ?? _stripDynamic(T);
    return mismatchDescription.add("is not an instance of '$name'");
  }
}

final _dart2DynamicArgs = RegExp('<dynamic(, dynamic)*>');

/// With this expression `{}.runtimeType.toString`,
/// Dart 1: "<Instance of Map>
/// Dart 2: "<Instance of Map<dynamic, dynamic>>"
///
/// This functions returns the Dart 1 output, when Dart 2 runtime semantics
/// are enabled.
String _stripDynamic(Type type) =>
    type.toString().replaceAll(_dart2DynamicArgs, '');
