// Copyright 2014 The Flutter Authors. 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:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:ui';

// ignore: deprecated_member_use
import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf;
// ignore: deprecated_member_use
import 'package:test_api/test_api.dart' as test_package show TypeMatcher;
import 'package:test_api/src/frontend/async_matcher.dart'; // ignore: implementation_imports

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show Card;
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage;
import 'accessibility.dart';
import 'binding.dart';
import 'finders.dart';
import 'goldens.dart';
import 'widget_tester.dart' show WidgetTester;

/// Asserts that the [Finder] matches no widgets in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsNothing);
/// ```
///
/// See also:
///
///  * [findsWidgets], when you want the finder to find one or more widgets.
///  * [findsOneWidget], when you want the finder to find exactly one widget.
///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
const Matcher findsNothing = _FindsWidgetMatcher(null, 0);

/// Asserts that the [Finder] locates at least one widget in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsWidgets);
/// ```
///
/// See also:
///
///  * [findsNothing], when you want the finder to not find anything.
///  * [findsOneWidget], when you want the finder to find exactly one widget.
///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
const Matcher findsWidgets = _FindsWidgetMatcher(1, null);

/// Asserts that the [Finder] locates at exactly one widget in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsOneWidget);
/// ```
///
/// See also:
///
///  * [findsNothing], when you want the finder to not find anything.
///  * [findsWidgets], when you want the finder to find one or more widgets.
///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1);

/// Asserts that the [Finder] locates the specified number of widgets in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsNWidgets(2));
/// ```
///
/// See also:
///
///  * [findsNothing], when you want the finder to not find anything.
///  * [findsWidgets], when you want the finder to find one or more widgets.
///  * [findsOneWidget], when you want the finder to find exactly one widget.
Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n);

/// Asserts that the [Finder] locates a single widget that has at
/// least one [Offstage] widget ancestor.
///
/// It's important to use a full finder, since by default finders exclude
/// offstage widgets.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save', skipOffstage: false), isOffstage);
/// ```
///
/// See also:
///
///  * [isOnstage], the opposite.
const Matcher isOffstage = _IsOffstage();

/// Asserts that the [Finder] locates a single widget that has no
/// [Offstage] widget ancestors.
///
/// See also:
///
///  * [isOffstage], the opposite.
const Matcher isOnstage = _IsOnstage();

/// Asserts that the [Finder] locates a single widget that has at
/// least one [Card] widget ancestor.
///
/// See also:
///
///  * [isNotInCard], the opposite.
const Matcher isInCard = _IsInCard();

/// Asserts that the [Finder] locates a single widget that has no
/// [Card] widget ancestors.
///
/// This is equivalent to `isNot(isInCard)`.
///
/// See also:
///
///  * [isInCard], the opposite.
const Matcher isNotInCard = _IsNotInCard();

/// Asserts that the object represents the same color as [color] when used to paint.
///
/// Specifically this matcher checks the object is of type [Color] and its [Color.value]
/// equals to that of the given [color].
Matcher isSameColorAs(Color color) => _ColorMatcher(targetColor: color);

/// Asserts that an object's toString() is a plausible one-line description.
///
/// Specifically, this matcher checks that the string does not contains newline
/// characters, and does not have leading or trailing whitespace, is not
/// empty, and does not contain the default `Instance of ...` string.
const Matcher hasOneLineDescription = _HasOneLineDescription();

/// Asserts that an object's toStringDeep() is a plausible multiline
/// description.
///
/// Specifically, this matcher checks that an object's
/// `toStringDeep(prefixLineOne, prefixOtherLines)`:
///
///  * Does not have leading or trailing whitespace.
///  * Does not contain the default `Instance of ...` string.
///  * The last line has characters other than tree connector characters and
///    whitespace. For example: the line ` │ ║ ╎` has only tree connector
///    characters and whitespace.
///  * Does not contain lines with trailing white space.
///  * Has multiple lines.
///  * The first line starts with `prefixLineOne`
///  * All subsequent lines start with `prefixOtherLines`.
const Matcher hasAGoodToStringDeep = _HasGoodToStringDeep();

/// A matcher for functions that throw [FlutterError].
///
/// This is equivalent to `throwsA(isA<FlutterError>())`.
///
/// If you are trying to test whether a call to [WidgetTester.pumpWidget]
/// results in a [FlutterError], see [TestWidgetsFlutterBinding.takeException].
///
/// See also:
///
///  * [throwsAssertionError], to test if a function throws any [AssertionError].
///  * [throwsArgumentError], to test if a functions throws an [ArgumentError].
///  * [isFlutterError], to test if any object is a [FlutterError].
final Matcher throwsFlutterError = throwsA(isFlutterError);

/// A matcher for functions that throw [AssertionError].
///
/// This is equivalent to `throwsA(isA<AssertionError>())`.
///
/// If you are trying to test whether a call to [WidgetTester.pumpWidget]
/// results in an [AssertionError], see
/// [TestWidgetsFlutterBinding.takeException].
///
/// See also:
///
///  * [throwsFlutterError], to test if a function throws a [FlutterError].
///  * [throwsArgumentError], to test if a functions throws an [ArgumentError].
///  * [isAssertionError], to test if any object is any kind of [AssertionError].
final Matcher throwsAssertionError = throwsA(isAssertionError);

/// A matcher for [FlutterError].
///
/// This is equivalent to `isInstanceOf<FlutterError>()`.
///
/// See also:
///
///  * [throwsFlutterError], to test if a function throws a [FlutterError].
///  * [isAssertionError], to test if any object is any kind of [AssertionError].
final test_package.TypeMatcher<FlutterError> isFlutterError = isA<FlutterError>();

/// A matcher for [AssertionError].
///
/// This is equivalent to `isInstanceOf<AssertionError>()`.
///
/// See also:
///
///  * [throwsAssertionError], to test if a function throws any [AssertionError].
///  * [isFlutterError], to test if any object is a [FlutterError].
final test_package.TypeMatcher<AssertionError> isAssertionError = isA<AssertionError>();

/// A matcher that compares the type of the actual value to the type argument T.
// TODO(ianh): Remove this once https://github.com/dart-lang/matcher/issues/98 is fixed
test_package.TypeMatcher<T> isInstanceOf<T>() => isA<T>();

/// Asserts that two [double]s are equal, within some tolerated error.
///
/// {@template flutter.flutter_test.moreOrLessEquals}
/// Two values are considered equal if the difference between them is within
/// [precisionErrorTolerance] of the larger one. This is an arbitrary value
/// which can be adjusted using the `epsilon` argument. This matcher is intended
/// to compare floating point numbers that are the result of different sequences
/// of operations, such that they may have accumulated slightly different
/// errors.
/// {@endtemplate}
///
/// See also:
///
///  * [closeTo], which is identical except that the epsilon argument is
///    required and not named.
///  * [inInclusiveRange], which matches if the argument is in a specified
///    range.
///  * [rectMoreOrLessEquals] and [offsetMoreOrLessEquals], which do something
///    similar but for [Rect]s and [Offset]s respectively.
Matcher moreOrLessEquals(double value, { double epsilon = precisionErrorTolerance }) {
  return _MoreOrLessEquals(value, epsilon);
}

/// Asserts that two [Rect]s are equal, within some tolerated error.
///
/// {@macro flutter.flutter_test.moreOrLessEquals}
///
/// See also:
///
///  * [moreOrLessEquals], which is for [double]s.
///  * [offsetMoreOrLessEquals], which is for [Offset]s.
///  * [within], which offers a generic version of this functionality that can
///    be used to match [Rect]s as well as other types.
Matcher rectMoreOrLessEquals(Rect value, { double epsilon = precisionErrorTolerance }) {
  return _IsWithinDistance<Rect>(_rectDistance, value, epsilon);
}

/// Asserts that two [Offset]s are equal, within some tolerated error.
///
/// {@macro flutter.flutter_test.moreOrLessEquals}
///
/// See also:
///
///  * [moreOrLessEquals], which is for [double]s.
///  * [rectMoreOrLessEquals], which is for [Rect]s.
///  * [within], which offers a generic version of this functionality that can
///    be used to match [Offset]s as well as other types.
Matcher offsetMoreOrLessEquals(Offset value, { double epsilon = precisionErrorTolerance }) {
  return _IsWithinDistance<Offset>(_offsetDistance, value, epsilon);
}

/// Asserts that two [String]s are equal after normalizing likely hash codes.
///
/// A `#` followed by 5 hexadecimal digits is assumed to be a short hash code
/// and is normalized to #00000.
///
/// See Also:
///
///  * [describeIdentity], a method that generates short descriptions of objects
///    with ids that match the pattern #[0-9a-f]{5}.
///  * [shortHash], a method that generates a 5 character long hexadecimal
///    [String] based on [Object.hashCode].
///  * [DiagnosticableTree.toStringDeep], a method that returns a [String]
///    typically containing multiple hash codes.
Matcher equalsIgnoringHashCodes(String value) {
  return _EqualsIgnoringHashCodes(value);
}

/// A matcher for [MethodCall]s, asserting that it has the specified
/// method [name] and [arguments].
///
/// Arguments checking implements deep equality for [List] and [Map] types.
Matcher isMethodCall(String name, { required dynamic arguments }) {
  return _IsMethodCall(name, arguments);
}

/// Asserts that 2 paths cover the same area by sampling multiple points.
///
/// Samples at least [sampleSize]^2 points inside [areaToCompare], and asserts
/// that the [Path.contains] method returns the same value for each of the
/// points for both paths.
///
/// When using this matcher you typically want to use a rectangle larger than
/// the area you expect to paint in for [areaToCompare] to catch errors where
/// the path draws outside the expected area.
Matcher coversSameAreaAs(Path expectedPath, { required Rect areaToCompare, int sampleSize = 20 })
  => _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);

/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the
/// golden image file identified by [key], with an optional [version] number.
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
/// treated as the image for the widget. As such, you may choose to wrap a test
/// widget in a [RepaintBoundary] to specify a particular focus for the test.
///
/// The [key] may be either a [Uri] or a [String] representation of a URL.
///
/// The [version] is a number that can be used to differentiate historical
/// golden files. This parameter is optional.
///
/// This is an asynchronous matcher, meaning that callers should use
/// [expectLater] when using this matcher and await the future returned by
/// [expectLater].
///
/// ## Golden File Testing
///
/// The term __golden file__ refers to a master image that is considered the true
/// rendering of a given widget, state, application, or other visual
/// representation you have chosen to capture.
///
/// The master golden image files that are tested against can be created or
/// updated by running `flutter test --update-goldens` on the test.
///
/// {@tool snippet}
/// Sample invocations of [matchesGoldenFile].
///
/// ```dart
/// await expectLater(
///   find.text('Save'),
///   matchesGoldenFile('save.png'),
/// );
///
/// await expectLater(
///   image,
///   matchesGoldenFile('save.png'),
/// );
///
/// await expectLater(
///   imageFuture,
///   matchesGoldenFile(
///     'save.png',
///     version: 2,
///   ),
/// );
///
/// await expectLater(
///   find.byType(MyWidget),
///   matchesGoldenFile('goldens/myWidget.png'),
/// );
/// ```
/// {@end-tool}
///
/// See also:
///
///  * [GoldenFileComparator], which acts as the backend for this matcher.
///  * [LocalFileComparator], which is the default [GoldenFileComparator]
///    implementation for `flutter test`.
///  * [matchesReferenceImage], which should be used instead if you want to
///    verify that two different code paths create identical images.
///  * [flutter_test] for a discussion of test configurations, whereby callers
///    may swap out the backend for this matcher.
AsyncMatcher matchesGoldenFile(Object key, {int? version}) {
  if (key is Uri) {
    return MatchesGoldenFile(key, version);
  } else if (key is String) {
    return MatchesGoldenFile.forStringPath(key, version);
  }
  throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
}

/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches a
/// reference image identified by [image].
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
/// treated as the image for the widget.
///
/// This is an asynchronous matcher, meaning that callers should use
/// [expectLater] when using this matcher and await the future returned by
/// [expectLater].
///
/// ## Sample code
///
/// ```dart
/// final ui.Paint paint = ui.Paint()
///   ..style = ui.PaintingStyle.stroke
///   ..strokeWidth = 1.0;
/// final ui.PictureRecorder recorder = ui.PictureRecorder();
/// final ui.Canvas pictureCanvas = ui.Canvas(recorder);
/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
/// final ui.Picture picture = recorder.endRecording();
/// ui.Image referenceImage = picture.toImage(50, 50);
///
/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage));
/// await expectLater(image, matchesReferenceImage(referenceImage);
/// await expectLater(imageFuture, matchesReferenceImage(referenceImage));
/// ```
///
/// See also:
///
///  * [matchesGoldenFile], which should be used instead if you need to verify
///    that a [Finder] or [ui.Image] matches a golden image.
AsyncMatcher matchesReferenceImage(ui.Image image) {
  return _MatchesReferenceImage(image);
}

/// Asserts that a [SemanticsNode] contains the specified information.
///
/// If either the label, hint, value, textDirection, or rect fields are not
/// provided, then they are not part of the comparison.  All of the boolean
/// flag and action fields must match, and default to false.
///
/// To retrieve the semantics data of a widget, use [WidgetTester.getSemantics]
/// with a [Finder] that returns a single widget. Semantics must be enabled
/// in order to use this method.
///
/// ## Sample code
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
/// expect(tester.getSemantics(find.text('hello')), matchesSemanticsNode(label: 'hello'));
/// handle.dispose();
/// ```
///
/// See also:
///
///   * [WidgetTester.getSemantics], the tester method which retrieves semantics.
Matcher matchesSemantics({
  String? label,
  String? hint,
  String? value,
  String? increasedValue,
  String? decreasedValue,
  TextDirection? textDirection,
  Rect? rect,
  Size? size,
  double? elevation,
  double? thickness,
  int? platformViewId,
  int? maxValueLength,
  int? currentValueLength,
  // Flags //
  bool hasCheckedState = false,
  bool isChecked = false,
  bool isSelected = false,
  bool isButton = false,
  bool isSlider = false,
  bool isLink = false,
  bool isFocused = false,
  bool isFocusable = false,
  bool isTextField = false,
  bool isReadOnly = false,
  bool hasEnabledState = false,
  bool isEnabled = false,
  bool isInMutuallyExclusiveGroup = false,
  bool isHeader = false,
  bool isObscured = false,
  bool isMultiline = false,
  bool namesRoute = false,
  bool scopesRoute = false,
  bool isHidden = false,
  bool isImage = false,
  bool isLiveRegion = false,
  bool hasToggledState = false,
  bool isToggled = false,
  bool hasImplicitScrolling = false,
  // Actions //
  bool hasTapAction = false,
  bool hasLongPressAction = false,
  bool hasScrollLeftAction = false,
  bool hasScrollRightAction = false,
  bool hasScrollUpAction = false,
  bool hasScrollDownAction = false,
  bool hasIncreaseAction = false,
  bool hasDecreaseAction = false,
  bool hasShowOnScreenAction = false,
  bool hasMoveCursorForwardByCharacterAction = false,
  bool hasMoveCursorBackwardByCharacterAction = false,
  bool hasMoveCursorForwardByWordAction = false,
  bool hasMoveCursorBackwardByWordAction = false,
  bool hasSetSelectionAction = false,
  bool hasCopyAction = false,
  bool hasCutAction = false,
  bool hasPasteAction = false,
  bool hasDidGainAccessibilityFocusAction = false,
  bool hasDidLoseAccessibilityFocusAction = false,
  bool hasDismissAction = false,
  // Custom actions and overrides
  String? onTapHint,
  String? onLongPressHint,
  List<CustomSemanticsAction>? customActions,
  List<Matcher>? children,
}) {
  final List<SemanticsFlag> flags = <SemanticsFlag>[
    if (hasCheckedState) SemanticsFlag.hasCheckedState,
    if (isChecked) SemanticsFlag.isChecked,
    if (isSelected) SemanticsFlag.isSelected,
    if (isButton) SemanticsFlag.isButton,
    if (isSlider) SemanticsFlag.isSlider,
    if (isLink) SemanticsFlag.isLink,
    if (isTextField) SemanticsFlag.isTextField,
    if (isReadOnly) SemanticsFlag.isReadOnly,
    if (isFocused) SemanticsFlag.isFocused,
    if (isFocusable) SemanticsFlag.isFocusable,
    if (hasEnabledState) SemanticsFlag.hasEnabledState,
    if (isEnabled) SemanticsFlag.isEnabled,
    if (isInMutuallyExclusiveGroup) SemanticsFlag.isInMutuallyExclusiveGroup,
    if (isHeader) SemanticsFlag.isHeader,
    if (isObscured) SemanticsFlag.isObscured,
    if (isMultiline) SemanticsFlag.isMultiline,
    if (namesRoute) SemanticsFlag.namesRoute,
    if (scopesRoute) SemanticsFlag.scopesRoute,
    if (isHidden) SemanticsFlag.isHidden,
    if (isImage) SemanticsFlag.isImage,
    if (isLiveRegion) SemanticsFlag.isLiveRegion,
    if (hasToggledState) SemanticsFlag.hasToggledState,
    if (isToggled) SemanticsFlag.isToggled,
    if (hasImplicitScrolling) SemanticsFlag.hasImplicitScrolling,
    if (isSlider) SemanticsFlag.isSlider
  ];

  final List<SemanticsAction> actions = <SemanticsAction>[
    if (hasTapAction) SemanticsAction.tap,
    if (hasLongPressAction) SemanticsAction.longPress,
    if (hasScrollLeftAction) SemanticsAction.scrollLeft,
    if (hasScrollRightAction) SemanticsAction.scrollRight,
    if (hasScrollUpAction) SemanticsAction.scrollUp,
    if (hasScrollDownAction) SemanticsAction.scrollDown,
    if (hasIncreaseAction) SemanticsAction.increase,
    if (hasDecreaseAction) SemanticsAction.decrease,
    if (hasShowOnScreenAction) SemanticsAction.showOnScreen,
    if (hasMoveCursorForwardByCharacterAction) SemanticsAction.moveCursorForwardByCharacter,
    if (hasMoveCursorBackwardByCharacterAction) SemanticsAction.moveCursorBackwardByCharacter,
    if (hasSetSelectionAction) SemanticsAction.setSelection,
    if (hasCopyAction) SemanticsAction.copy,
    if (hasCutAction) SemanticsAction.cut,
    if (hasPasteAction) SemanticsAction.paste,
    if (hasDidGainAccessibilityFocusAction) SemanticsAction.didGainAccessibilityFocus,
    if (hasDidLoseAccessibilityFocusAction) SemanticsAction.didLoseAccessibilityFocus,
    if (customActions != null && customActions.isNotEmpty) SemanticsAction.customAction,
    if (hasDismissAction) SemanticsAction.dismiss,
    if (hasMoveCursorForwardByWordAction) SemanticsAction.moveCursorForwardByWord,
    if (hasMoveCursorBackwardByWordAction) SemanticsAction.moveCursorBackwardByWord,
  ];
  SemanticsHintOverrides? hintOverrides;
  if (onTapHint != null || onLongPressHint != null)
    hintOverrides = SemanticsHintOverrides(
      onTapHint: onTapHint,
      onLongPressHint: onLongPressHint,
    );

  return _MatchesSemanticsData(
    label: label,
    hint: hint,
    value: value,
    increasedValue: increasedValue,
    decreasedValue: decreasedValue,
    actions: actions,
    flags: flags,
    textDirection: textDirection,
    rect: rect,
    size: size,
    elevation: elevation,
    thickness: thickness,
    platformViewId: platformViewId,
    customActions: customActions,
    hintOverrides: hintOverrides,
    currentValueLength: currentValueLength,
    maxValueLength: maxValueLength,
    children: children,
  );
}

/// Asserts that the currently rendered widget meets the provided accessibility
/// `guideline`.
///
/// This matcher requires the result to be awaited and for semantics to be
/// enabled first.
///
/// ## Sample code
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
/// await expectLater(tester, meetsGuideline(textContrastGuideline));
/// handle.dispose();
/// ```
///
/// Supported accessibility guidelines:
///
///   * [androidTapTargetGuideline], for Android minimum tappable area guidelines.
///   * [iOSTapTargetGuideline], for iOS minimum tappable area guidelines.
///   * [textContrastGuideline], for WCAG minimum text contrast guidelines.
AsyncMatcher meetsGuideline(AccessibilityGuideline guideline) {
  return _MatchesAccessibilityGuideline(guideline);
}

/// The inverse matcher of [meetsGuideline].
///
/// This is needed because the [isNot] matcher does not compose with an
/// [AsyncMatcher].
AsyncMatcher doesNotMeetGuideline(AccessibilityGuideline guideline) {
  return _DoesNotMatchAccessibilityGuideline(guideline);
}

class _FindsWidgetMatcher extends Matcher {
  const _FindsWidgetMatcher(this.min, this.max);

  final int? min;
  final int? max;

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
    assert(min != null || max != null);
    assert(min == null || max == null || min! <= max!);
    matchState[Finder] = finder;
    int count = 0;
    final Iterator<Element> iterator = finder.evaluate().iterator;
    if (min != null) {
      while (count < min! && iterator.moveNext())
        count += 1;
      if (count < min!)
        return false;
    }
    if (max != null) {
      while (count <= max! && iterator.moveNext())
        count += 1;
      if (count > max!)
        return false;
    }
    return true;
  }

  @override
  Description describe(Description description) {
    assert(min != null || max != null);
    if (min == max) {
      if (min == 1)
        return description.add('exactly one matching node in the widget tree');
      return description.add('exactly $min matching nodes in the widget tree');
    }
    if (min == null) {
      if (max == 0)
        return description.add('no matching nodes in the widget tree');
      if (max == 1)
        return description.add('at most one matching node in the widget tree');
      return description.add('at most $max matching nodes in the widget tree');
    }
    if (max == null) {
      if (min == 1)
        return description.add('at least one matching node in the widget tree');
      return description.add('at least $min matching nodes in the widget tree');
    }
    return description.add('between $min and $max matching nodes in the widget tree (inclusive)');
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    final Finder finder = matchState[Finder] as Finder;
    final int count = finder.evaluate().length;
    if (count == 0) {
      assert(min != null && min! > 0);
      if (min == 1 && max == 1)
        return mismatchDescription.add('means none were found but one was expected');
      return mismatchDescription.add('means none were found but some were expected');
    }
    if (max == 0) {
      if (count == 1)
        return mismatchDescription.add('means one was found but none were expected');
      return mismatchDescription.add('means some were found but none were expected');
    }
    if (min != null && count < min!)
      return mismatchDescription.add('is not enough');
    assert(max != null && count > min!);
    return mismatchDescription.add('is too many');
  }
}

bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) {
  final Iterable<Element> nodes = finder.evaluate();
  if (nodes.length != 1)
    return false;
  bool result = false;
  nodes.single.visitAncestorElements((Element ancestor) {
    if (predicate(ancestor.widget)) {
      result = true;
      return false;
    }
    return true;
  });
  return result;
}

bool _hasAncestorOfType(Finder finder, Type targetType) {
  return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
}

class _IsOffstage extends Matcher {
  const _IsOffstage();

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
    return _hasAncestorMatching(finder, (Widget widget) {
      if (widget is Offstage)
        return widget.offstage;
      return false;
    });
  }

  @override
  Description describe(Description description) => description.add('offstage');
}

class _IsOnstage extends Matcher {
  const _IsOnstage();

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
    final Iterable<Element> nodes = finder.evaluate();
    if (nodes.length != 1)
      return false;
    bool result = true;
    nodes.single.visitAncestorElements((Element ancestor) {
      final Widget widget = ancestor.widget;
      if (widget is Offstage) {
        result = !widget.offstage;
        return false;
      }
      return true;
    });
    return result;
  }

  @override
  Description describe(Description description) => description.add('onstage');
}

class _IsInCard extends Matcher {
  const _IsInCard();

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => _hasAncestorOfType(finder, Card);

  @override
  Description describe(Description description) => description.add('in card');
}

class _IsNotInCard extends Matcher {
  const _IsNotInCard();

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => !_hasAncestorOfType(finder, Card);

  @override
  Description describe(Description description) => description.add('not in card');
}

class _HasOneLineDescription extends Matcher {
  const _HasOneLineDescription();

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
    final String description = object.toString();
    return description.isNotEmpty
        && !description.contains('\n')
        && !description.contains('Instance of ')
        && description.trim() == description;
  }

  @override
  Description describe(Description description) => description.add('one line description');
}

class _EqualsIgnoringHashCodes extends Matcher {
  _EqualsIgnoringHashCodes(String v) : _value = _normalize(v);

  final String _value;

  static final Object _mismatchedValueKey = Object();

  static String _normalize(String s) {
    return s.replaceAll(RegExp(r'#[0-9a-fA-F]{5}'), '#00000');
  }

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
    final String description = _normalize(object as String);
    if (_value != description) {
      matchState[_mismatchedValueKey] = description;
      return false;
    }
    return true;
  }

  @override
  Description describe(Description description) {
    return description.add('multi line description equals $_value');
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    if (matchState.containsKey(_mismatchedValueKey)) {
      final String actualValue = matchState[_mismatchedValueKey] as String;
      // Leading whitespace is added so that lines in the multiline
      // description returned by addDescriptionOf are all indented equally
      // which makes the output easier to read for this case.
      return mismatchDescription
          .add('expected normalized value\n  ')
          .addDescriptionOf(_value)
          .add('\nbut got\n  ')
          .addDescriptionOf(actualValue);
    }
    return mismatchDescription;
  }
}

/// Returns true if [c] represents a whitespace code unit.
bool _isWhitespace(int c) => (c <= 0x000D && c >= 0x0009) || c == 0x0020;

/// Returns true if [c] represents a vertical line Unicode line art code unit.
///
/// See [https://en.wikipedia.org/wiki/Box-drawing_character]. This method only
/// specifies vertical line art code units currently used by Flutter line art.
/// There are other line art characters that technically also represent vertical
/// lines.
bool _isVerticalLine(int c) {
  return c == 0x2502 || c == 0x2503 || c == 0x2551 || c == 0x254e;
}

/// Returns whether a [line] is all vertical tree connector characters.
///
/// Example vertical tree connector characters: `│ ║ ╎`.
/// The last line of a text tree contains only vertical tree connector
/// characters indicates a poorly formatted tree.
bool _isAllTreeConnectorCharacters(String line) {
  for (int i = 0; i < line.length; ++i) {
    final int c = line.codeUnitAt(i);
    if (!_isWhitespace(c) && !_isVerticalLine(c))
      return false;
  }
  return true;
}

class _HasGoodToStringDeep extends Matcher {
  const _HasGoodToStringDeep();

  static final Object _toStringDeepErrorDescriptionKey = Object();

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
    final List<String> issues = <String>[];
    String description = object.toStringDeep() as String;
    if (description.endsWith('\n')) {
      // Trim off trailing \n as the remaining calculations assume
      // the description does not end with a trailing \n.
      description = description.substring(0, description.length - 1);
    } else {
      issues.add('Not terminated with a line break.');
    }

    if (description.trim() != description)
      issues.add('Has trailing whitespace.');

    final List<String> lines = description.split('\n');
    if (lines.length < 2)
      issues.add('Does not have multiple lines.');

    if (description.contains('Instance of '))
      issues.add('Contains text "Instance of ".');

    for (int i = 0; i < lines.length; ++i) {
      final String line = lines[i];
      if (line.isEmpty)
        issues.add('Line ${i+1} is empty.');

      if (line.trimRight() != line)
        issues.add('Line ${i+1} has trailing whitespace.');
    }

    if (_isAllTreeConnectorCharacters(lines.last))
      issues.add('Last line is all tree connector characters.');

    // If a toStringDeep method doesn't properly handle nested values that
    // contain line breaks it can fail to add the required prefixes to all
    // lined when toStringDeep is called specifying prefixes.
    const String prefixLineOne    = 'PREFIX_LINE_ONE____';
    const String prefixOtherLines = 'PREFIX_OTHER_LINES_';
    final List<String> prefixIssues = <String>[];
    String descriptionWithPrefixes =
      object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines) as String;
    if (descriptionWithPrefixes.endsWith('\n')) {
      // Trim off trailing \n as the remaining calculations assume
      // the description does not end with a trailing \n.
      descriptionWithPrefixes = descriptionWithPrefixes.substring(
          0, descriptionWithPrefixes.length - 1);
    }
    final List<String> linesWithPrefixes = descriptionWithPrefixes.split('\n');
    if (!linesWithPrefixes.first.startsWith(prefixLineOne))
      prefixIssues.add('First line does not contain expected prefix.');

    for (int i = 1; i < linesWithPrefixes.length; ++i) {
      if (!linesWithPrefixes[i].startsWith(prefixOtherLines))
        prefixIssues.add('Line ${i+1} does not contain the expected prefix.');
    }

    final StringBuffer errorDescription = StringBuffer();
    if (issues.isNotEmpty) {
      errorDescription.writeln('Bad toStringDeep():');
      errorDescription.writeln(description);
      errorDescription.writeAll(issues, '\n');
    }

    if (prefixIssues.isNotEmpty) {
      errorDescription.writeln(
          'Bad toStringDeep(prefixLineOne: "$prefixLineOne", prefixOtherLines: "$prefixOtherLines"):');
      errorDescription.writeln(descriptionWithPrefixes);
      errorDescription.writeAll(prefixIssues, '\n');
    }

    if (errorDescription.isNotEmpty) {
      matchState[_toStringDeepErrorDescriptionKey] =
          errorDescription.toString();
      return false;
    }
    return true;
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    if (matchState.containsKey(_toStringDeepErrorDescriptionKey)) {
      return mismatchDescription.add(matchState[_toStringDeepErrorDescriptionKey] as String);
    }
    return mismatchDescription;
  }

  @override
  Description describe(Description description) {
    return description.add('multi line description');
  }
}

/// Computes the distance between two values.
///
/// The distance should be a metric in a metric space (see
/// https://en.wikipedia.org/wiki/Metric_space). Specifically, if `f` is a
/// distance function then the following conditions should hold:
///
/// - f(a, b) >= 0
/// - f(a, b) == 0 if and only if a == b
/// - f(a, b) == f(b, a)
/// - f(a, c) <= f(a, b) + f(b, c), known as triangle inequality
///
/// This makes it useful for comparing numbers, [Color]s, [Offset]s and other
/// sets of value for which a metric space is defined.
typedef DistanceFunction<T> = num Function(T a, T b);

/// The type of a union of instances of [DistanceFunction<T>] for various types
/// T.
///
/// This type is used to describe a collection of [DistanceFunction<T>]
/// functions which have (potentially) unrelated argument types. Since the
/// argument types of the functions may be unrelated, their type is declared as
/// `Never`, which is the bottom type in dart to which all other types can be
/// assigned to.
///
/// Calling an instance of this type must either be done dynamically, or by
/// first casting it to a [DistanceFunction<T>] for some concrete T.
typedef AnyDistanceFunction = num Function(Never a, Never b);

const Map<Type, AnyDistanceFunction> _kStandardDistanceFunctions = <Type, AnyDistanceFunction>{
  Color: _maxComponentColorDistance,
  HSVColor: _maxComponentHSVColorDistance,
  HSLColor: _maxComponentHSLColorDistance,
  Offset: _offsetDistance,
  int: _intDistance,
  double: _doubleDistance,
  Rect: _rectDistance,
  Size: _sizeDistance,
};

int _intDistance(int a, int b) => (b - a).abs();
double _doubleDistance(double a, double b) => (b - a).abs();
double _offsetDistance(Offset a, Offset b) => (b - a).distance;

double _maxComponentColorDistance(Color a, Color b) {
  int delta = math.max<int>((a.red - b.red).abs(), (a.green - b.green).abs());
  delta = math.max<int>(delta, (a.blue - b.blue).abs());
  delta = math.max<int>(delta, (a.alpha - b.alpha).abs());
  return delta.toDouble();
}

// Compares hue by converting it to a 0.0 - 1.0 range, so that the comparison
// can be a similar error percentage per component.
double _maxComponentHSVColorDistance(HSVColor a, HSVColor b) {
  double delta = math.max<double>((a.saturation - b.saturation).abs(), (a.value - b.value).abs());
  delta = math.max<double>(delta, ((a.hue - b.hue) / 360.0).abs());
  return math.max<double>(delta, (a.alpha - b.alpha).abs());
}

// Compares hue by converting it to a 0.0 - 1.0 range, so that the comparison
// can be a similar error percentage per component.
double _maxComponentHSLColorDistance(HSLColor a, HSLColor b) {
  double delta = math.max<double>((a.saturation - b.saturation).abs(), (a.lightness - b.lightness).abs());
  delta = math.max<double>(delta, ((a.hue - b.hue) / 360.0).abs());
  return math.max<double>(delta, (a.alpha - b.alpha).abs());
}

double _rectDistance(Rect a, Rect b) {
  double delta = math.max<double>((a.left - b.left).abs(), (a.top - b.top).abs());
  delta = math.max<double>(delta, (a.right - b.right).abs());
  delta = math.max<double>(delta, (a.bottom - b.bottom).abs());
  return delta;
}

double _sizeDistance(Size a, Size b) {
  // TODO(a14n): remove ignore when lint is updated, https://github.com/dart-lang/linter/issues/1843
  // ignore: unnecessary_parenthesis
  final Offset delta = (b - a) as Offset;
  return delta.distance;
}

/// Asserts that two values are within a certain distance from each other.
///
/// The distance is computed by a [DistanceFunction].
///
/// If `distanceFunction` is null, a standard distance function is used for the
/// `T` generic argument. Standard functions are defined for the following
/// types:
///
///  * [Color], whose distance is the maximum component-wise delta.
///  * [Offset], whose distance is the Euclidean distance computed using the
///    method [Offset.distance].
///  * [Rect], whose distance is the maximum component-wise delta.
///  * [Size], whose distance is the [Offset.distance] of the offset computed as
///    the difference between two sizes.
///  * [int], whose distance is the absolute difference between two integers.
///  * [double], whose distance is the absolute difference between two doubles.
///
/// See also:
///
///  * [moreOrLessEquals], which is similar to this function, but specializes in
///    [double]s and has an optional `epsilon` parameter.
///  * [rectMoreOrLessEquals], which is similar to this function, but
///    specializes in [Rect]s and has an optional `epsilon` parameter.
///  * [closeTo], which specializes in numbers only.
Matcher within<T>({
  required num distance,
  required T from,
  DistanceFunction<T>? distanceFunction,
}) {
  distanceFunction ??= _kStandardDistanceFunctions[T] as DistanceFunction<T>?;

  if (distanceFunction == null) {
    throw ArgumentError(
      'The specified distanceFunction was null, and a standard distance '
      'function was not found for type ${from.runtimeType} of the provided '
      '`from` argument.'
    );
  }

  return _IsWithinDistance<T>(distanceFunction, from, distance);
}

class _IsWithinDistance<T> extends Matcher {
  const _IsWithinDistance(this.distanceFunction, this.value, this.epsilon);

  final DistanceFunction<T> distanceFunction;
  final T value;
  final num epsilon;

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
    if (object is! T)
      return false;
    if (object == value)
      return true;
    final num distance = distanceFunction(object, value);
    if (distance < 0) {
      throw ArgumentError(
        'Invalid distance function was used to compare a ${value.runtimeType} '
        'to a ${object.runtimeType}. The function must return a non-negative '
        'double value, but it returned $distance.'
      );
    }
    matchState['distance'] = distance;
    return distance <= epsilon;
  }

  @override
  Description describe(Description description) => description.add('$value (±$epsilon)');

  @override
  Description describeMismatch(
    dynamic object,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    mismatchDescription.add('was ${matchState['distance']} away from the desired value.');
    return mismatchDescription;
  }
}

class _MoreOrLessEquals extends Matcher {
  const _MoreOrLessEquals(this.value, this.epsilon)
    : assert(epsilon >= 0);

  final double value;
  final double epsilon;

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
    if (object is! double)
      return false;
    if (object == value)
      return true;
    return (object - value).abs() <= epsilon;
  }

  @override
  Description describe(Description description) => description.add('$value (±$epsilon)');

  @override
  Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
    return super.describeMismatch(item, mismatchDescription, matchState, verbose)
      ..add('$item is not in the range of $value (±$epsilon).');
  }
}

class _IsMethodCall extends Matcher {
  const _IsMethodCall(this.name, this.arguments);

  final String name;
  final dynamic arguments;

  @override
  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
    if (item is! MethodCall)
      return false;
    if (item.method != name)
      return false;
    return _deepEquals(item.arguments, arguments);
  }

  bool _deepEquals(dynamic a, dynamic b) {
    if (a == b)
      return true;
    if (a is List)
      return b is List && _deepEqualsList(a, b);
    if (a is Map)
      return b is Map && _deepEqualsMap(a, b);
    return false;
  }

  bool _deepEqualsList(List<dynamic> a, List<dynamic> b) {
    if (a.length != b.length)
      return false;
    for (int i = 0; i < a.length; i++) {
      if (!_deepEquals(a[i], b[i]))
        return false;
    }
    return true;
  }

  bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
    if (a.length != b.length)
      return false;
    for (final dynamic key in a.keys) {
      if (!b.containsKey(key) || !_deepEquals(a[key], b[key]))
        return false;
    }
    return true;
  }

  @override
  Description describe(Description description) {
    return description
        .add('has method name: ').addDescriptionOf(name)
        .add(' with arguments: ').addDescriptionOf(arguments);
  }
}

/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderClipRect] with no clipper set, or an equivalent
/// [RenderClipPath].
const Matcher clipsWithBoundingRect = _ClipsWithBoundingRect();

/// Asserts that a [Finder] locates a single object whose root RenderObject is
/// not a [RenderClipRect], [RenderClipRRect], [RenderClipOval], or
/// [RenderClipPath].
const Matcher hasNoImmediateClip = _MatchAnythingExceptClip();

/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderClipRRect] with no clipper set, and border radius equals to
/// [borderRadius], or an equivalent [RenderClipPath].
Matcher clipsWithBoundingRRect({ required BorderRadius borderRadius }) {
  return _ClipsWithBoundingRRect(borderRadius: borderRadius);
}

/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderClipPath] with a [ShapeBorderClipper] that clips to
/// [shape].
Matcher clipsWithShapeBorder({ required ShapeBorder shape }) {
  return _ClipsWithShapeBorder(shape: shape);
}

/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderPhysicalModel] or a [RenderPhysicalShape].
///
/// - If the render object is a [RenderPhysicalModel]
///    - If [shape] is non null asserts that [RenderPhysicalModel.shape] is equal to
///   [shape].
///    - If [borderRadius] is non null asserts that [RenderPhysicalModel.borderRadius] is equal to
///   [borderRadius].
///     - If [elevation] is non null asserts that [RenderPhysicalModel.elevation] is equal to
///   [elevation].
/// - If the render object is a [RenderPhysicalShape]
///    - If [borderRadius] is non null asserts that the shape is a rounded
///   rectangle with this radius.
///    - If [borderRadius] is null, asserts that the shape is equivalent to
///   [shape].
///    - If [elevation] is non null asserts that [RenderPhysicalModel.elevation] is equal to
///   [elevation].
Matcher rendersOnPhysicalModel({
  BoxShape? shape,
  BorderRadius? borderRadius,
  double? elevation,
}) {
  return _RendersOnPhysicalModel(
    shape: shape,
    borderRadius: borderRadius,
    elevation: elevation,
  );
}

/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is [RenderPhysicalShape] that uses a [ShapeBorderClipper] that clips to
/// [shape] as its clipper.
/// If [elevation] is non null asserts that [RenderPhysicalShape.elevation] is
/// equal to [elevation].
Matcher rendersOnPhysicalShape({
  required ShapeBorder shape,
  double? elevation,
}) {
  return _RendersOnPhysicalShape(
    shape: shape,
    elevation: elevation,
  );
}

abstract class _FailWithDescriptionMatcher extends Matcher {
  const _FailWithDescriptionMatcher();

  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
    matchState['failure'] = description;
    return false;
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    return mismatchDescription.add(matchState['failure'] as String);
  }
}

class _MatchAnythingExceptClip extends _FailWithDescriptionMatcher {
  const _MatchAnythingExceptClip();

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
    final Iterable<Element> nodes = finder.evaluate();
    if (nodes.length != 1)
      return failWithDescription(matchState, 'did not have a exactly one child element');
    final RenderObject renderObject = nodes.single.renderObject!;

    switch (renderObject.runtimeType) {
      case RenderClipPath:
      case RenderClipOval:
      case RenderClipRect:
      case RenderClipRRect:
        return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
      default:
        return true;
    }
  }

  @override
  Description describe(Description description) {
    return description.add('does not have a clip as an immediate child');
  }
}

abstract class _MatchRenderObject<M extends RenderObject, T extends RenderObject> extends _FailWithDescriptionMatcher {
  const _MatchRenderObject();

  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, T renderObject);
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, M renderObject);

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
    final Iterable<Element> nodes = finder.evaluate();
    if (nodes.length != 1)
      return failWithDescription(matchState, 'did not have a exactly one child element');
    final RenderObject renderObject = nodes.single.renderObject!;

    if (renderObject.runtimeType == T)
      return renderObjectMatchesT(matchState, renderObject as T);

    if (renderObject.runtimeType == M)
      return renderObjectMatchesM(matchState, renderObject as M);

    return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
  }
}

class _RendersOnPhysicalModel extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
  const _RendersOnPhysicalModel({
    this.shape,
    this.borderRadius,
    this.elevation,
  });

  final BoxShape? shape;
  final BorderRadius? borderRadius;
  final double? elevation;

  @override
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
    if (shape != null && renderObject.shape != shape)
      return failWithDescription(matchState, 'had shape: ${renderObject.shape}');

    if (borderRadius != null && renderObject.borderRadius != borderRadius)
      return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');

    if (elevation != null && renderObject.elevation != elevation)
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');

    return true;
  }

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;

    if (borderRadius != null && !assertRoundedRectangle(shapeClipper, borderRadius!, matchState))
      return false;

    if (
      borderRadius == null &&
      shape == BoxShape.rectangle &&
      !assertRoundedRectangle(shapeClipper, BorderRadius.zero, matchState)
    ) {
      return false;
    }

    if (
      borderRadius == null &&
      shape == BoxShape.circle &&
      !assertCircle(shapeClipper, matchState)
    ) {
      return false;
    }

    if (elevation != null && renderObject.elevation != elevation)
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');

    return true;
  }

  bool assertRoundedRectangle(ShapeBorderClipper shapeClipper, BorderRadius borderRadius, Map<dynamic, dynamic> matchState) {
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder)
      return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
    if (border.borderRadius != borderRadius)
      return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
    return true;
  }

  bool assertCircle(ShapeBorderClipper shapeClipper, Map<dynamic, dynamic> matchState) {
    if (shapeClipper.shape.runtimeType != CircleBorder)
      return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
    return true;
  }

  @override
  Description describe(Description description) {
    description.add('renders on a physical model');
    if (shape != null)
      description.add(' with shape $shape');
    if (borderRadius != null)
      description.add(' with borderRadius $borderRadius');
    if (elevation != null)
      description.add(' with elevation $elevation');
    return description;
  }
}

class _RendersOnPhysicalShape extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
  const _RendersOnPhysicalShape({
    required this.shape,
    this.elevation,
  });

  final ShapeBorder shape;
  final double? elevation;

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;

    if (shapeClipper.shape != shape)
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');

    if (elevation != null && renderObject.elevation != elevation)
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');

    return true;
  }

  @override
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
    return false;
  }

  @override
  Description describe(Description description) {
    description.add('renders on a physical model with shape $shape');
    if (elevation != null)
      description.add(' with elevation $elevation');
    return description;
  }
}

class _ClipsWithBoundingRect extends _MatchRenderObject<RenderClipPath, RenderClipRect> {
  const _ClipsWithBoundingRect();

  @override
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRect renderObject) {
    if (renderObject.clipper != null)
      return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
    return true;
  }

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder)
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
    if (border.borderRadius != BorderRadius.zero)
      return failWithDescription(matchState, 'borderRadius was: ${border.borderRadius}');
    return true;
  }

  @override
  Description describe(Description description) =>
    description.add('clips with bounding rectangle');
}

class _ClipsWithBoundingRRect extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
  const _ClipsWithBoundingRRect({required this.borderRadius});

  final BorderRadius borderRadius;


  @override
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
    if (renderObject.clipper != null)
      return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');

    if (renderObject.borderRadius != borderRadius)
      return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');

    return true;
  }

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder)
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
    if (border.borderRadius != borderRadius)
      return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
    return true;
  }

  @override
  Description describe(Description description) =>
    description.add('clips with bounding rounded rectangle with borderRadius: $borderRadius');
}

class _ClipsWithShapeBorder extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
  const _ClipsWithShapeBorder({required this.shape});

  final ShapeBorder shape;

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
    if (shapeClipper.shape != shape)
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
    return true;
  }

  @override
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
    return false;
  }


  @override
  Description describe(Description description) =>
    description.add('clips with shape: $shape');
}

class _CoversSameAreaAs extends Matcher {
  _CoversSameAreaAs(
    this.expectedPath, {
    required this.areaToCompare,
    this.sampleSize = 20,
  }) : maxHorizontalNoise = areaToCompare.width / sampleSize,
       maxVerticalNoise = areaToCompare.height / sampleSize {
    // Use a fixed random seed to make sure tests are deterministic.
    random = math.Random(1);
  }

  final Path expectedPath;
  final Rect areaToCompare;
  final int sampleSize;
  final double maxHorizontalNoise;
  final double maxVerticalNoise;
  late math.Random random;

  @override
  bool matches(covariant Path actualPath, Map<dynamic, dynamic> matchState) {
    for (int i = 0; i < sampleSize; i += 1) {
      for (int j = 0; j < sampleSize; j += 1) {
        final Offset offset = Offset(
          i * (areaToCompare.width / sampleSize),
          j * (areaToCompare.height / sampleSize),
        );

        if (!_samplePoint(matchState, actualPath, offset))
          return false;

        final Offset noise = Offset(
          maxHorizontalNoise * random.nextDouble(),
          maxVerticalNoise * random.nextDouble(),
        );

        if (!_samplePoint(matchState, actualPath, offset + noise))
          return false;
      }
    }
    return true;
  }

  bool _samplePoint(Map<dynamic, dynamic> matchState, Path actualPath, Offset offset) {
    if (expectedPath.contains(offset) == actualPath.contains(offset))
      return true;

    if (actualPath.contains(offset))
      return failWithDescription(matchState, '$offset is contained in the actual path but not in the expected path');
    else
      return failWithDescription(matchState, '$offset is contained in the expected path but not in the actual path');
  }

  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
    matchState['failure'] = description;
    return false;
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    return mismatchDescription.add(matchState['failure'] as String);
  }

  @override
  Description describe(Description description) =>
    description.add('covers expected area and only expected area');
}

class _ColorMatcher extends Matcher {
  const _ColorMatcher({
    required this.targetColor,
  }) : assert(targetColor != null);

  final Color targetColor;

  @override
  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
    if (item is Color)
      return item == targetColor || item.value == targetColor.value;
    return false;
  }

  @override
  Description describe(Description description) => description.add('matches color $targetColor');
}

int _countDifferentPixels(Uint8List imageA, Uint8List imageB) {
  assert(imageA.length == imageB.length);
  int delta = 0;
  for (int i = 0; i < imageA.length; i+=4) {
    if (imageA[i] != imageB[i] ||
      imageA[i+1] != imageB[i+1] ||
      imageA[i+2] != imageB[i+2] ||
      imageA[i+3] != imageB[i+3]) {
      delta++;
    }
  }
  return delta;
}

class _MatchesReferenceImage extends AsyncMatcher {
  const _MatchesReferenceImage(this.referenceImage);

  final ui.Image referenceImage;

  @override
  Future<String?> matchAsync(dynamic item) async {
    Future<ui.Image> imageFuture;
    if (item is Future<ui.Image>) {
      imageFuture = item;
    } else if (item is ui.Image) {
      imageFuture = Future<ui.Image>.value(item);
    } else {
      final Finder finder = item as Finder;
      final Iterable<Element> elements = finder.evaluate();
      if (elements.isEmpty) {
        return 'could not be rendered because no widget was found';
      } else if (elements.length > 1) {
        return 'matched too many widgets';
      }
      imageFuture = captureImage(elements.single);
    }

    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding;
    return binding.runAsync<String?>(() async {
      final ui.Image image = await imageFuture;
      final ByteData? bytes = await image.toByteData();
      if (bytes == null)
        return 'could not be encoded.';

      final ByteData? referenceBytes = await referenceImage.toByteData();
      if (referenceBytes == null)
        return 'could not have its reference image encoded.';

      if (referenceImage.height != image.height || referenceImage.width != image.width)
        return 'does not match as width or height do not match. $image != $referenceImage';

      final int countDifferentPixels = _countDifferentPixels(
        Uint8List.view(bytes.buffer),
        Uint8List.view(referenceBytes.buffer),
      );
      return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
    }, additionalTime: const Duration(minutes: 1));
  }

  @override
  Description describe(Description description) {
    return description.add('rasterized image matches that of a $referenceImage reference image');
  }
}

class _MatchesSemanticsData extends Matcher {
  _MatchesSemanticsData({
    this.label,
    this.value,
    this.increasedValue,
    this.decreasedValue,
    this.hint,
    this.flags,
    this.actions,
    this.textDirection,
    this.rect,
    this.size,
    this.elevation,
    this.thickness,
    this.platformViewId,
    this.maxValueLength,
    this.currentValueLength,
    this.customActions,
    this.hintOverrides,
    this.children,
  });

  final String? label;
  final String? value;
  final String? hint;
  final String? increasedValue;
  final String? decreasedValue;
  final SemanticsHintOverrides? hintOverrides;
  final List<SemanticsAction>? actions;
  final List<CustomSemanticsAction>? customActions;
  final List<SemanticsFlag>? flags;
  final TextDirection? textDirection;
  final Rect? rect;
  final Size? size;
  final double? elevation;
  final double? thickness;
  final int? platformViewId;
  final int? maxValueLength;
  final int? currentValueLength;
  final List<Matcher>? children;

  @override
  Description describe(Description description) {
    description.add('has semantics');
    if (label != null)
      description.add(' with label: $label');
    if (value != null)
      description.add(' with value: $value');
    if (hint != null)
      description.add(' with hint: $hint');
    if (increasedValue != null)
      description.add(' with increasedValue: $increasedValue ');
    if (decreasedValue != null)
      description.add(' with decreasedValue: $decreasedValue ');
    if (actions != null)
      description.add(' with actions: ').addDescriptionOf(actions);
    if (flags != null)
      description.add(' with flags: ').addDescriptionOf(flags);
    if (textDirection != null)
      description.add(' with textDirection: $textDirection ');
    if (rect != null)
      description.add(' with rect: $rect');
    if (size != null)
      description.add(' with size: $size');
    if (elevation != null)
      description.add(' with elevation: $elevation');
    if (thickness != null)
      description.add(' with thickness: $thickness');
    if (platformViewId != null)
      description.add(' with platformViewId: $platformViewId');
    if (maxValueLength != null)
      description.add(' with maxValueLength: $maxValueLength');
    if (currentValueLength != null)
      description.add(' with currentValueLength: $currentValueLength');
    if (customActions != null)
      description.add(' with custom actions: $customActions');
    if (hintOverrides != null)
      description.add(' with custom hints: $hintOverrides');
    if (children != null) {
      description.add(' with children:\n');
      for (final _MatchesSemanticsData child in children!.cast<_MatchesSemanticsData>())
        child.describe(description);
    }
    return description;
  }


  @override
  bool matches(dynamic node, Map<dynamic, dynamic> matchState) {
    // TODO(jonahwilliams): remove dynamic once we have removed getSemanticsData.
    if (node == null)
      return failWithDescription(matchState, 'No SemanticsData provided. '
        'Maybe you forgot to enable semantics?');
    final SemanticsData data = node is SemanticsNode ? node.getSemanticsData() : (node as SemanticsData);
    if (label != null && label != data.label)
      return failWithDescription(matchState, 'label was: ${data.label}');
    if (hint != null && hint != data.hint)
      return failWithDescription(matchState, 'hint was: ${data.hint}');
    if (value != null && value != data.value)
      return failWithDescription(matchState, 'value was: ${data.value}');
    if (increasedValue != null && increasedValue != data.increasedValue)
      return failWithDescription(matchState, 'increasedValue was: ${data.increasedValue}');
    if (decreasedValue != null && decreasedValue != data.decreasedValue)
      return failWithDescription(matchState, 'decreasedValue was: ${data.decreasedValue}');
    if (textDirection != null && textDirection != data.textDirection)
      return failWithDescription(matchState, 'textDirection was: $textDirection');
    if (rect != null && rect != data.rect)
      return failWithDescription(matchState, 'rect was: ${data.rect}');
    if (size != null && size != data.rect.size)
      return failWithDescription(matchState, 'size was: ${data.rect.size}');
    if (elevation != null && elevation != data.elevation)
      return failWithDescription(matchState, 'elevation was: ${data.elevation}');
    if (thickness != null && thickness != data.thickness)
      return failWithDescription(matchState, 'thickness was: ${data.thickness}');
    if (platformViewId != null && platformViewId != data.platformViewId)
      return failWithDescription(matchState, 'platformViewId was: ${data.platformViewId}');
    if (currentValueLength != null && currentValueLength != data.currentValueLength)
      return failWithDescription(matchState, 'currentValueLength was: ${data.currentValueLength}');
    if (maxValueLength != null && maxValueLength != data.maxValueLength)
      return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}');
    if (actions != null) {
      int actionBits = 0;
      for (final SemanticsAction action in actions!)
        actionBits |= action.index;
      if (actionBits != data.actions) {
        final List<String> actionSummary = <String>[
          for (final SemanticsAction action in SemanticsAction.values.values)
            if ((data.actions & action.index) != 0)
              describeEnum(action),
        ];
        return failWithDescription(matchState, 'actions were: $actionSummary');
      }
    }
    if (customActions != null || hintOverrides != null) {
      final List<CustomSemanticsAction> providedCustomActions = data.customSemanticsActionIds?.map<CustomSemanticsAction>((int id) {
        return CustomSemanticsAction.getAction(id)!;
      }).toList() ?? <CustomSemanticsAction>[];
      final List<CustomSemanticsAction> expectedCustomActions = customActions?.toList() ?? <CustomSemanticsAction>[];
      if (hintOverrides?.onTapHint != null)
        expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides!.onTapHint!, action: SemanticsAction.tap));
      if (hintOverrides?.onLongPressHint != null)
        expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides!.onLongPressHint!, action: SemanticsAction.longPress));
      if (expectedCustomActions.length != providedCustomActions.length)
        return failWithDescription(matchState, 'custom actions where: $providedCustomActions');
      int sortActions(CustomSemanticsAction left, CustomSemanticsAction right) {
        return CustomSemanticsAction.getIdentifier(left) - CustomSemanticsAction.getIdentifier(right);
      }
      expectedCustomActions.sort(sortActions);
      providedCustomActions.sort(sortActions);
      for (int i = 0; i < expectedCustomActions.length; i++) {
        if (expectedCustomActions[i] != providedCustomActions[i])
          return failWithDescription(matchState, 'custom actions where: $providedCustomActions');
      }
    }
    if (flags != null) {
      int flagBits = 0;
      for (final SemanticsFlag flag in flags!)
        flagBits |= flag.index;
      if (flagBits != data.flags) {
        final List<String> flagSummary = <String>[
          for (final SemanticsFlag flag in SemanticsFlag.values.values)
            if ((data.flags & flag.index) != 0)
              describeEnum(flag),
        ];
        return failWithDescription(matchState, 'flags were: $flagSummary');
      }
    }
    bool allMatched = true;
    if (children != null) {
      int i = 0;
      node.visitChildren((SemanticsNode child) {
        allMatched = children![i].matches(child, matchState) && allMatched;
        i += 1;
        return allMatched;
      });
    }
    return allMatched;
  }

  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
    matchState['failure'] = description;
    return false;
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    return mismatchDescription.add(matchState['failure'] as String);
  }
}

class _MatchesAccessibilityGuideline extends AsyncMatcher {
  _MatchesAccessibilityGuideline(this.guideline);

  final AccessibilityGuideline guideline;

  @override
  Description describe(Description description) {
    return description.add(guideline.description);
  }

  @override
  Future<String?> matchAsync(covariant WidgetTester tester) async {
    final Evaluation result = await guideline.evaluate(tester);
    if (result.passed)
      return null;
    return result.reason;
  }
}

class _DoesNotMatchAccessibilityGuideline extends AsyncMatcher {
  _DoesNotMatchAccessibilityGuideline(this.guideline);

  final AccessibilityGuideline guideline;

  @override
  Description describe(Description description) {
    return description.add('Does not ' + guideline.description);
  }

  @override
  Future<String?> matchAsync(covariant WidgetTester tester) async {
    final Evaluation result = await guideline.evaluate(tester);
    if (result.passed)
      return 'Failed';
    return null;
  }
}
