// 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.

// flutter_ignore_for_file: golden_tag (see analyze.dart)

import 'dart:math' as math;
import 'dart:typed_data';

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix3;

/// Class that makes it easy to mock common toStringDeep behavior.
class _MockToStringDeep {
  _MockToStringDeep(String str) : _lines = <String>[] {
    final List<String> lines = str.split('\n');
    for (int i = 0; i < lines.length - 1; ++i) {
      _lines.add('${lines[i]}\n');
    }

    // If the last line is empty, that really just means that the previous
    // line was terminated with a line break.
    if (lines.isNotEmpty && lines.last.isNotEmpty) {
      _lines.add(lines.last);
    }
  }

  _MockToStringDeep.fromLines(this._lines);

  /// Lines in the message to display when [toStringDeep] is called.
  /// For correct toStringDeep behavior, each line should be terminated with a
  /// line break.
  final List<String> _lines;

  String toStringDeep({ String prefixLineOne = '', String prefixOtherLines = '' }) {
    final StringBuffer sb = StringBuffer();
    if (_lines.isNotEmpty) {
      sb.write('$prefixLineOne${_lines.first}');
    }

    for (int i = 1; i < _lines.length; ++i) {
      sb.write('$prefixOtherLines${_lines[i]}');
    }

    return sb.toString();
  }

  @override
  String toString() => toStringDeep();
}

void main() {
  test('hasOneLineDescription', () {
    expect('Hello', hasOneLineDescription);
    expect('Hello\nHello', isNot(hasOneLineDescription));
    expect(' Hello', isNot(hasOneLineDescription));
    expect('Hello ', isNot(hasOneLineDescription));
    expect(Object(), isNot(hasOneLineDescription));
  });

  test('hasAGoodToStringDeep', () {
    expect(_MockToStringDeep('Hello\n World\n'), hasAGoodToStringDeep);
    // Not terminated with a line break.
    expect(_MockToStringDeep('Hello\n World'), isNot(hasAGoodToStringDeep));
    // Trailing whitespace on last line.
    expect(_MockToStringDeep('Hello\n World \n'),
        isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('Hello\n World\t\n'),
        isNot(hasAGoodToStringDeep));
    // Leading whitespace on line 1.
    expect(_MockToStringDeep(' Hello\n World \n'),
        isNot(hasAGoodToStringDeep));

    // Single line.
    expect(_MockToStringDeep('Hello World'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('Hello World\n'), isNot(hasAGoodToStringDeep));

    expect(_MockToStringDeep('Hello: World\nFoo: bar\n'),
        hasAGoodToStringDeep);
    expect(_MockToStringDeep('Hello: World\nFoo: 42\n'),
        hasAGoodToStringDeep);
    // Contains default Object.toString().
    expect(_MockToStringDeep('Hello: World\nFoo: ${Object()}\n'),
        isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n'), hasAGoodToStringDeep);
    expect(_MockToStringDeep('A\n├─B\n╘══════\n'), hasAGoodToStringDeep);
    // Last line is all whitespace or vertical line art.
    expect(_MockToStringDeep('A\n├─B\n\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n│\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n│\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n│\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n╎\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n║\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n │\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n ╎\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n ║\n'), isNot(hasAGoodToStringDeep));
    expect(_MockToStringDeep('A\n├─B\n ││\n'), isNot(hasAGoodToStringDeep));

    expect(_MockToStringDeep(
        'A\n'
        '├─B\n'
        '│\n'
        '└─C\n'), hasAGoodToStringDeep);
    // Last line is all whitespace or vertical line art.
    expect(_MockToStringDeep(
        'A\n'
        '├─B\n'
        '│\n'), isNot(hasAGoodToStringDeep));

    expect(
      _MockToStringDeep.fromLines(<String>[
        'Paragraph#00000\n',
        ' │ size: (400x200)\n',
        ' ╘═╦══ text ═══\n',
        '   ║ TextSpan:\n',
        '   ║   "I polished up that handle so carefullee\n',
        '   ║   That now I am the Ruler of the Queen\'s Navee!"\n',
        '   ╚═══════════\n',
      ]),
      hasAGoodToStringDeep,
    );

    // Text span
    expect(
      _MockToStringDeep.fromLines(<String>[
        'Paragraph#00000\n',
        ' │ size: (400x200)\n',
        ' ╘═╦══ text ═══\n',
        '   ║ TextSpan:\n',
        '   ║   "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen\'s Navee!"\n',
        '   ╚═══════════\n',
      ]),
      isNot(hasAGoodToStringDeep),
    );
  });

  test('equalsIgnoringHashCodes', () {
    expect('Foo#34219', equalsIgnoringHashCodes('Foo#00000'));
    expect('Foo#34219', equalsIgnoringHashCodes('Foo#12345'));
    expect('Foo#34219', equalsIgnoringHashCodes('Foo#abcdf'));
    expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo')));
    expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#')));
    expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#0')));
    expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#00')));
    expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#00000 ')));
    expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#000000')));
    expect('Foo#34219', isNot(equalsIgnoringHashCodes('Foo#123456')));

    expect('Foo#34219:', equalsIgnoringHashCodes('Foo#00000:'));
    expect('Foo#34219:', isNot(equalsIgnoringHashCodes('Foo#00000')));

    expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#00000'));
    expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#12345'));
    expect('Foo#a3b4d', equalsIgnoringHashCodes('Foo#abcdf'));
    expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo')));
    expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#')));
    expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#0')));
    expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#00')));
    expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#00000 ')));
    expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#000000')));
    expect('Foo#a3b4d', isNot(equalsIgnoringHashCodes('Foo#123456')));

    expect('FOO#A3b4D', equalsIgnoringHashCodes('FOO#00000'));
    expect('FOO#A3b4J', isNot(equalsIgnoringHashCodes('FOO#00000')));

    expect('Foo#12345(Bar#9110f)',
        equalsIgnoringHashCodes('Foo#00000(Bar#00000)'));
    expect('Foo#12345(Bar#9110f)',
        isNot(equalsIgnoringHashCodes('Foo#00000(Bar#)')));

    expect('Foo', isNot(equalsIgnoringHashCodes('Foo#00000')));
    expect('Foo#', isNot(equalsIgnoringHashCodes('Foo#00000')));
    expect('Foo#3421', isNot(equalsIgnoringHashCodes('Foo#00000')));
    expect('Foo#342193', isNot(equalsIgnoringHashCodes('Foo#00000')));
    expect(<String>['Foo#a3b4d'], equalsIgnoringHashCodes(<String>['Foo#12345']));
    expect(
      <String>['Foo#a3b4d', 'Foo#12345'],
      equalsIgnoringHashCodes(<String>['Foo#00000', 'Foo#00000']),
    );
    expect(
      <String>['Foo#a3b4d', 'Bar#12345'],
      equalsIgnoringHashCodes(<String>['Foo#00000', 'Bar#00000']),
    );
    expect(
      <String>['Foo#a3b4d', 'Bar#12345'],
      isNot(equalsIgnoringHashCodes(<String>['Bar#00000', 'Foo#00000'])),
    );
    expect(<String>['Foo#a3b4d'], isNot(equalsIgnoringHashCodes(<String>['Foo'])));
    expect(
      <String>['Foo#a3b4d'],
      isNot(equalsIgnoringHashCodes(<String>['Foo#00000', 'Bar#00000'])),
    );
  });

  test('moreOrLessEquals', () {
    expect(0.0, moreOrLessEquals(1e-11));
    expect(1e-11, moreOrLessEquals(0.0));
    expect(-1e-11, moreOrLessEquals(0.0));

    expect(0.0, isNot(moreOrLessEquals(1e11)));
    expect(1e11, isNot(moreOrLessEquals(0.0)));
    expect(-1e11, isNot(moreOrLessEquals(0.0)));

    expect(0.0, isNot(moreOrLessEquals(1.0)));
    expect(1.0, isNot(moreOrLessEquals(0.0)));
    expect(-1.0, isNot(moreOrLessEquals(0.0)));

    expect(1e-11, moreOrLessEquals(-1e-11));
    expect(-1e-11, moreOrLessEquals(1e-11));

    expect(11.0, isNot(moreOrLessEquals(-11.0, epsilon: 1.0)));
    expect(-11.0, isNot(moreOrLessEquals(11.0, epsilon: 1.0)));

    expect(11.0, moreOrLessEquals(-11.0, epsilon: 100.0));
    expect(-11.0, moreOrLessEquals(11.0, epsilon: 100.0));

    expect(0, moreOrLessEquals(0.0));
    expect(0.0, moreOrLessEquals(0));
  });

  test('matrixMoreOrLessEquals', () {
    expect(
      Matrix4.rotationZ(math.pi),
      matrixMoreOrLessEquals(Matrix4.fromList(<double>[
       -1,  0, 0, 0,
        0, -1, 0, 0,
        0,  0, 1, 0,
        0,  0, 0, 1,
      ]))
    );

    expect(
      Matrix4.rotationZ(math.pi),
      matrixMoreOrLessEquals(Matrix4.fromList(<double>[
       -2,  0, 0, 0,
        0, -2, 0, 0,
        0,  0, 1, 0,
        0,  0, 0, 1,
      ]), epsilon: 2)
    );

    expect(
      Matrix4.rotationZ(math.pi),
      isNot(matrixMoreOrLessEquals(Matrix4.fromList(<double>[
       -2,  0, 0, 0,
        0, -2, 0, 0,
        0,  0, 1, 0,
        0,  0, 0, 1,
      ])))
    );
  });

  test('matrix3MoreOrLessEquals', () {
    expect(
      Matrix3.rotationZ(math.pi),
      matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
       -1,  0, 0,
        0, -1, 0,
        0,  0, 1,
      ]))
    );

    expect(
      Matrix3.rotationZ(math.pi),
      matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
       -2,  0, 0,
        0, -2, 0,
        0,  0, 1,
      ]), epsilon: 2)
    );

    expect(
      Matrix3.rotationZ(math.pi),
      isNot(matrix3MoreOrLessEquals(Matrix3.fromList(<double>[
       -2,  0, 0,
        0, -2, 0,
        0,  0, 1,
      ])))
    );
  });

  test('rectMoreOrLessEquals', () {
    expect(
      const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
      rectMoreOrLessEquals(const Rect.fromLTRB(0.0, 0.0, 10.0, 10.00000000001)),
    );

    expect(
      const Rect.fromLTRB(11.0, 11.0, 20.0, 20.0),
      isNot(rectMoreOrLessEquals(const Rect.fromLTRB(-11.0, -11.0, 20.0, 20.0), epsilon: 1.0)),
    );

    expect(
      const Rect.fromLTRB(11.0, 11.0, 20.0, 20.0),
      rectMoreOrLessEquals(const Rect.fromLTRB(-11.0, -11.0, 20.0, 20.0), epsilon: 100.0),
    );
  });

  test('within', () {
    expect(0.0, within<double>(distance: 0.1, from: 0.05));
    expect(0.0, isNot(within<double>(distance: 0.1, from: 0.2)));

    expect(0, within<int>(distance: 1, from: 1));
    expect(0, isNot(within<int>(distance: 1, from: 2)));

    expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x01000000)));
    expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00010000)));
    expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00000100)));
    expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00000001)));
    expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x01010101)));
    expect(const Color(0x00000000), isNot(within<Color>(distance: 1, from: const Color(0x02000000))));

    expect(const Offset(1.0, 0.0), within(distance: 1.0, from: Offset.zero));
    expect(const Offset(1.0, 0.0), isNot(within(distance: 1.0, from: const Offset(-1.0, 0.0))));

    expect(const Rect.fromLTRB(0.0, 1.0, 2.0, 3.0), within<Rect>(distance: 4.0, from: const Rect.fromLTRB(1.0, 3.0, 5.0, 7.0)));
    expect(const Rect.fromLTRB(0.0, 1.0, 2.0, 3.0), isNot(within<Rect>(distance: 3.9, from: const Rect.fromLTRB(1.0, 3.0, 5.0, 7.0))));

    expect(const Size(1.0, 1.0), within<Size>(distance: 1.415, from: const Size(2.0, 2.0)));
    expect(const Size(1.0, 1.0), isNot(within<Size>(distance: 1.414, from: const Size(2.0, 2.0))));

    expect(
      () => within<bool>(distance: 1, from: false),
      throwsArgumentError,
    );

    expect(
      () => within<int>(distance: 1, from: 2, distanceFunction: (int a, int b) => -1).matches(1, <dynamic, dynamic>{}),
      throwsArgumentError,
    );
  });

  test('isSameColorAs', () {
    expect(
      const Color(0x87654321),
      isSameColorAs(const _CustomColor(0x87654321)),
    );

    expect(
      const _CustomColor(0x87654321),
      isSameColorAs(const Color(0x87654321)),
    );

    expect(
      const Color(0x12345678),
      isNot(isSameColorAs(const _CustomColor(0x87654321))),
    );

    expect(
      const _CustomColor(0x87654321),
      isNot(isSameColorAs(const Color(0x12345678))),
    );

    expect(
      const _CustomColor(0xFF123456),
      isSameColorAs(const _CustomColor(0xFF123456, isEqual: false)),
    );
  });

  group('coversSameAreaAs', () {
    test('empty Paths', () {
      expect(
        Path(),
        coversSameAreaAs(
          Path(),
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        ),
      );
    });

    test('mismatch', () {
      final Path rectPath = Path()
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
      expect(
        Path(),
        isNot(coversSameAreaAs(
          rectPath,
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        )),
      );
    });

    test('mismatch out of examined area', () {
      final Path rectPath = Path()
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
      rectPath.addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
      expect(
        Path(),
        coversSameAreaAs(
          rectPath,
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 4.0, 4.0),
        ),
      );
    });

    test('differently constructed rects match', () {
      final Path rectPath = Path()
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
      final Path linePath = Path()
        ..moveTo(5.0, 5.0)
        ..lineTo(5.0, 6.0)
        ..lineTo(6.0, 6.0)
        ..lineTo(6.0, 5.0)
        ..close();
      expect(
        linePath,
        coversSameAreaAs(
          rectPath,
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        ),
      );
    });

    test('partially overlapping paths', () {
      final Path rectPath = Path()
        ..addRect(const Rect.fromLTRB(5.0, 5.0, 6.0, 6.0));
      final Path linePath = Path()
        ..moveTo(5.0, 5.0)
        ..lineTo(5.0, 6.0)
        ..lineTo(6.0, 6.0)
        ..lineTo(6.0, 5.5)
        ..close();
      expect(
        linePath,
        isNot(coversSameAreaAs(
          rectPath,
          areaToCompare: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        )),
      );
    });
  });

  group('matchesGoldenFile', () {
    late _FakeComparator comparator;

    Widget boilerplate(Widget child) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: child,
      );
    }

    setUp(() {
      comparator = _FakeComparator();
      goldenFileComparator = comparator;
    });

    group('matches', () {
      testWidgets('if comparator succeeds', (WidgetTester tester) async {
        await tester.pumpWidget(boilerplate(const Text('hello')));
        final Finder finder = find.byType(Text);
        await expectLater(finder, matchesGoldenFile('foo.png'));
        expect(comparator.invocation, _ComparatorInvocation.compare);
        expect(comparator.imageBytes, hasLength(greaterThan(0)));
        expect(comparator.golden, Uri.parse('foo.png'));
      });

      testWidgets('list of integers', (WidgetTester tester) async {
        await expectLater(<int>[1, 2], matchesGoldenFile('foo.png'));
        expect(comparator.invocation, _ComparatorInvocation.compare);
        expect(comparator.imageBytes, equals(<int>[1, 2]));
        expect(comparator.golden, Uri.parse('foo.png'));
      });

      testWidgets('future list of integers', (WidgetTester tester) async {
        await expectLater(Future<List<int>>.value(<int>[1, 2]), matchesGoldenFile('foo.png'));
        expect(comparator.invocation, _ComparatorInvocation.compare);
        expect(comparator.imageBytes, equals(<int>[1, 2]));
        expect(comparator.golden, Uri.parse('foo.png'));
      });

      testWidgets('future nullable list of integers',
          (WidgetTester tester) async {
        await expectLater(Future<List<int>?>.value(<int>[1, 2]), matchesGoldenFile('foo.png'));
        expect(comparator.invocation, _ComparatorInvocation.compare);
        expect(comparator.imageBytes, equals(<int>[1, 2]));
        expect(comparator.golden, Uri.parse('foo.png'));
      });
    });

    group('does not match', () {
      testWidgets('if comparator returns false', (WidgetTester tester) async {
        comparator.behavior = _ComparatorBehavior.returnFalse;
        await tester.pumpWidget(boilerplate(const Text('hello')));
        final Finder finder = find.byType(Text);
        await expectLater(
          () => expectLater(finder, matchesGoldenFile('foo.png')),
          throwsA(isA<TestFailure>().having(
            (TestFailure error) => error.message,
            'message',
            contains('does not match'),
          )),
        );
        expect(comparator.invocation, _ComparatorInvocation.compare);
      });

      testWidgets('if comparator throws', (WidgetTester tester) async {
        comparator.behavior = _ComparatorBehavior.throwTestFailure;
        await tester.pumpWidget(boilerplate(const Text('hello')));
        final Finder finder = find.byType(Text);
        await expectLater(
          () => expectLater(finder, matchesGoldenFile('foo.png')),
          throwsA(isA<TestFailure>().having(
            (TestFailure error) => error.message,
            'message',
            contains('fake message'),
          )),
        );
        expect(comparator.invocation, _ComparatorInvocation.compare);
      });

      testWidgets('if finder finds no widgets', (WidgetTester tester) async {
        await tester.pumpWidget(boilerplate(Container()));
        final Finder finder = find.byType(Text);
        await expectLater(
          () => expectLater(finder, matchesGoldenFile('foo.png')),
          throwsA(isA<TestFailure>().having(
            (TestFailure error) => error.message,
            'message',
            contains('no widget was found'),
          )),
        );
        expect(comparator.invocation, isNull);
      });

      testWidgets('if finder finds multiple widgets', (WidgetTester tester) async {
        await tester.pumpWidget(boilerplate(const Column(
          children: <Widget>[Text('hello'), Text('world')],
        )));
        final Finder finder = find.byType(Text);
        await expectLater(
          () => expectLater(finder, matchesGoldenFile('foo.png')),
          throwsA(isA<TestFailure>().having(
            (TestFailure error) => error.message,
            'message',
            contains('too many widgets'),
          )),
        );
        expect(comparator.invocation, isNull);
      });
    });

    testWidgets('calls update on comparator if autoUpdateGoldenFiles is true', (WidgetTester tester) async {
      autoUpdateGoldenFiles = true;
      await tester.pumpWidget(boilerplate(const Text('hello')));
      final Finder finder = find.byType(Text);
      await expectLater(finder, matchesGoldenFile('foo.png'));
      expect(comparator.invocation, _ComparatorInvocation.update);
      expect(comparator.imageBytes, hasLength(greaterThan(0)));
      expect(comparator.golden, Uri.parse('foo.png'));
      autoUpdateGoldenFiles = false;
    });
  });

  group('matchesSemanticsData', () {
    testWidgets('matches SemanticsData', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      const Key key = Key('semantics');
      await tester.pumpWidget(Semantics(
        key: key,
        namesRoute: true,
        header: true,
        button: true,
        link: true,
        onTap: () { },
        onLongPress: () { },
        label: 'foo',
        hint: 'bar',
        value: 'baz',
        increasedValue: 'a',
        decreasedValue: 'b',
        textDirection: TextDirection.rtl,
        onTapHint: 'scan',
        onLongPressHint: 'fill',
        customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
          const CustomSemanticsAction(label: 'foo'): () { },
          const CustomSemanticsAction(label: 'bar'): () { },
        },
      ));

      expect(tester.getSemantics(find.byKey(key)),
        matchesSemantics(
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          increasedValue: 'a',
          decreasedValue: 'b',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isLink: true,
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
            const CustomSemanticsAction(label: 'bar'),
          ],
        ),
      );

      // Doesn't match custom actions
      expect(tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isLink: true,
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
            const CustomSemanticsAction(label: 'barz'),
          ],
        )),
      );

      // Doesn't match wrong hints
      expect(tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isLink: true,
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scans',
          onLongPressHint: 'fills',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
            const CustomSemanticsAction(label: 'bar'),
          ],
        )),
      );

      handle.dispose();
    });

    testWidgets('Can match all semantics flags and actions', (WidgetTester tester) async {
      int actions = 0;
      int flags = 0;
      const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
      for (final SemanticsAction action in SemanticsAction.values) {
        actions |= action.index;
      }
      for (final SemanticsFlag flag in SemanticsFlag.values) {
        flags |= flag.index;
      }
      final SemanticsData data = SemanticsData(
        flags: flags,
        actions: actions,
        attributedLabel: AttributedString('a'),
        attributedIncreasedValue: AttributedString('b'),
        attributedValue: AttributedString('c'),
        attributedDecreasedValue: AttributedString('d'),
        attributedHint: AttributedString('e'),
        tooltip: 'f',
        textDirection: TextDirection.ltr,
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        elevation: 3.0,
        thickness: 4.0,
        textSelection: null,
        scrollIndex: null,
        scrollChildCount: null,
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
        platformViewId: 105,
        customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
        currentValueLength: 10,
        maxValueLength: 15,
      );
      final _FakeSemanticsNode node = _FakeSemanticsNode(data);

      expect(node, matchesSemantics(
         rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
         size: const Size(10.0, 10.0),
         elevation: 3.0,
         thickness: 4.0,
         platformViewId: 105,
         currentValueLength: 10,
         maxValueLength: 15,
         /* Flags */
         hasCheckedState: true,
         isChecked: true,
         isCheckStateMixed: true,
         isSelected: true,
         isButton: true,
         isSlider: true,
         isKeyboardKey: true,
         isLink: true,
         isTextField: true,
         isReadOnly: true,
         hasEnabledState: true,
         isFocused: true,
         isFocusable: true,
         isEnabled: true,
         isInMutuallyExclusiveGroup: true,
         isHeader: true,
         isObscured: true,
         isMultiline: true,
         namesRoute: true,
         scopesRoute: true,
         isHidden: true,
         isImage: true,
         isLiveRegion: true,
         hasToggledState: true,
         isToggled: true,
         hasImplicitScrolling: true,
         hasExpandedState: true,
         isExpanded: true,
         /* Actions */
         hasTapAction: true,
         hasLongPressAction: true,
         hasScrollLeftAction: true,
         hasScrollRightAction: true,
         hasScrollUpAction: true,
         hasScrollDownAction: true,
         hasIncreaseAction: true,
         hasDecreaseAction: true,
         hasShowOnScreenAction: true,
         hasMoveCursorForwardByCharacterAction: true,
         hasMoveCursorBackwardByCharacterAction: true,
         hasMoveCursorForwardByWordAction: true,
         hasMoveCursorBackwardByWordAction: true,
         hasSetTextAction: true,
         hasSetSelectionAction: true,
         hasCopyAction: true,
         hasCutAction: true,
         hasPasteAction: true,
         hasDidGainAccessibilityFocusAction: true,
         hasDidLoseAccessibilityFocusAction: true,
         hasDismissAction: true,
         customActions: <CustomSemanticsAction>[action],
      ));
    });

    testWidgets('Can match child semantics', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      const Key key = Key('a');
      await tester.pumpWidget(Semantics(
        key: key,
        label: 'Foo',
        container: true,
        explicitChildNodes: true,
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'Bar',
          textDirection: TextDirection.ltr,
        ),
      ));
      final SemanticsNode node = tester.getSemantics(find.byKey(key));

      expect(node, matchesSemantics(
        label: 'Foo',
        textDirection: TextDirection.ltr,
        children: <Matcher>[
          matchesSemantics(
            label: 'Bar',
            textDirection: TextDirection.ltr,
          ),
        ],
      ));
      handle.dispose();
    });

    testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();

      const Key key = Key('semantics');
      await tester.pumpWidget(Semantics(
        key: key,
        namesRoute: true,
        header: true,
        button: true,
        link: true,
        onTap: () { },
        onLongPress: () { },
        label: 'foo',
        hint: 'bar',
        value: 'baz',
        increasedValue: 'a',
        decreasedValue: 'b',
        textDirection: TextDirection.rtl,
        onTapHint: 'scan',
        onLongPressHint: 'fill',
        customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
          const CustomSemanticsAction(label: 'foo'): () { },
          const CustomSemanticsAction(label: 'bar'): () { },
        },
      ));

      // This should fail due to the mis-match between the `namesRoute` value.
      void failedExpectation() => expect(tester.getSemantics(find.byKey(key)),
        matchesSemantics(
          // Adding the explicit `false` for test readability
          // ignore: avoid_redundant_argument_values
          namesRoute: false,
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          increasedValue: 'a',
          decreasedValue: 'b',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isLink: true,
          isHeader: true,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
            const CustomSemanticsAction(label: 'bar'),
          ],
        ),
      );

      expect(failedExpectation, throwsA(isA<TestFailure>()));
      handle.dispose();
    });
  });

  group('containsSemantics', () {
    testWidgets('matches SemanticsData', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();

      const Key key = Key('semantics');
      await tester.pumpWidget(Semantics(
        key: key,
        namesRoute: true,
        header: true,
        button: true,
        link: true,
        onTap: () { },
        onLongPress: () { },
        label: 'foo',
        hint: 'bar',
        value: 'baz',
        increasedValue: 'a',
        decreasedValue: 'b',
        textDirection: TextDirection.rtl,
        onTapHint: 'scan',
        onLongPressHint: 'fill',
        customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
          const CustomSemanticsAction(label: 'foo'): () { },
          const CustomSemanticsAction(label: 'bar'): () { },
        },
      ));

      expect(
        tester.getSemantics(find.byKey(key)),
        containsSemantics(
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          increasedValue: 'a',
          decreasedValue: 'b',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isLink: true,
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
            const CustomSemanticsAction(label: 'bar'),
          ],
        ),
      );

      expect(
        tester.getSemantics(find.byKey(key)),
        isNot(containsSemantics(
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isLink: true,
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
            const CustomSemanticsAction(label: 'barz'),
          ],
        )),
        reason: 'CustomSemanticsAction "barz" should not have matched "bar".'
      );

      expect(
        tester.getSemantics(find.byKey(key)),
        isNot(matchesSemantics(
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isLink: true,
          isHeader: true,
          namesRoute: true,
          onTapHint: 'scans',
          onLongPressHint: 'fills',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
            const CustomSemanticsAction(label: 'bar'),
          ],
        )),
        reason: 'onTapHint "scans" should not have matched "scan".',
      );
      handle.dispose();
    });

    testWidgets('can match all semantics flags and actions enabled', (WidgetTester tester) async {
      int actions = 0;
      int flags = 0;
      const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
      for (final SemanticsAction action in SemanticsAction.values) {
        actions |= action.index;
      }
      for (final SemanticsFlag flag in SemanticsFlag.values) {
        flags |= flag.index;
      }
      final SemanticsData data = SemanticsData(
        flags: flags,
        actions: actions,
        attributedLabel: AttributedString('a'),
        attributedIncreasedValue: AttributedString('b'),
        attributedValue: AttributedString('c'),
        attributedDecreasedValue: AttributedString('d'),
        attributedHint: AttributedString('e'),
        tooltip: 'f',
        textDirection: TextDirection.ltr,
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        elevation: 3.0,
        thickness: 4.0,
        textSelection: null,
        scrollIndex: null,
        scrollChildCount: null,
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
        platformViewId: 105,
        customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
        currentValueLength: 10,
        maxValueLength: 15,
      );
      final _FakeSemanticsNode node = _FakeSemanticsNode(data);

      expect(
        node,
        containsSemantics(
          rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
          size: const Size(10.0, 10.0),
          elevation: 3.0,
          thickness: 4.0,
          platformViewId: 105,
          currentValueLength: 10,
          maxValueLength: 15,
          /* Flags */
          hasCheckedState: true,
          isChecked: true,
          isSelected: true,
          isButton: true,
          isSlider: true,
          isKeyboardKey: true,
          isLink: true,
          isTextField: true,
          isReadOnly: true,
          hasEnabledState: true,
          isFocused: true,
          isFocusable: true,
          isEnabled: true,
          isInMutuallyExclusiveGroup: true,
          isHeader: true,
          isObscured: true,
          isMultiline: true,
          namesRoute: true,
          scopesRoute: true,
          isHidden: true,
          isImage: true,
          isLiveRegion: true,
          hasToggledState: true,
          isToggled: true,
          hasImplicitScrolling: true,
          hasExpandedState: true,
          isExpanded: true,
          /* Actions */
          hasTapAction: true,
          hasLongPressAction: true,
          hasScrollLeftAction: true,
          hasScrollRightAction: true,
          hasScrollUpAction: true,
          hasScrollDownAction: true,
          hasIncreaseAction: true,
          hasDecreaseAction: true,
          hasShowOnScreenAction: true,
          hasMoveCursorForwardByCharacterAction: true,
          hasMoveCursorBackwardByCharacterAction: true,
          hasMoveCursorForwardByWordAction: true,
          hasMoveCursorBackwardByWordAction: true,
          hasSetTextAction: true,
          hasSetSelectionAction: true,
          hasCopyAction: true,
          hasCutAction: true,
          hasPasteAction: true,
          hasDidGainAccessibilityFocusAction: true,
          hasDidLoseAccessibilityFocusAction: true,
          hasDismissAction: true,
          customActions: <CustomSemanticsAction>[action],
        ),
      );
    });

    testWidgets('can match all flags and actions disabled', (WidgetTester tester) async {
      final SemanticsData data = SemanticsData(
        flags: 0,
        actions: 0,
        attributedLabel: AttributedString('a'),
        attributedIncreasedValue: AttributedString('b'),
        attributedValue: AttributedString('c'),
        attributedDecreasedValue: AttributedString('d'),
        attributedHint: AttributedString('e'),
        tooltip: 'f',
        textDirection: TextDirection.ltr,
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        elevation: 3.0,
        thickness: 4.0,
        textSelection: null,
        scrollIndex: null,
        scrollChildCount: null,
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
        platformViewId: 105,
        currentValueLength: 10,
        maxValueLength: 15,
      );
      final _FakeSemanticsNode node = _FakeSemanticsNode(data);

      expect(
        node,
        containsSemantics(
          rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
          size: const Size(10.0, 10.0),
          elevation: 3.0,
          thickness: 4.0,
          platformViewId: 105,
          currentValueLength: 10,
          maxValueLength: 15,
          /* Flags */
          hasCheckedState: false,
          isChecked: false,
          isSelected: false,
          isButton: false,
          isSlider: false,
          isKeyboardKey: false,
          isLink: false,
          isTextField: false,
          isReadOnly: false,
          hasEnabledState: false,
          isFocused: false,
          isFocusable: false,
          isEnabled: false,
          isInMutuallyExclusiveGroup: false,
          isHeader: false,
          isObscured: false,
          isMultiline: false,
          namesRoute: false,
          scopesRoute: false,
          isHidden: false,
          isImage: false,
          isLiveRegion: false,
          hasToggledState: false,
          isToggled: false,
          hasImplicitScrolling: false,
          hasExpandedState: false,
          isExpanded: false,
          /* Actions */
          hasTapAction: false,
          hasLongPressAction: false,
          hasScrollLeftAction: false,
          hasScrollRightAction: false,
          hasScrollUpAction: false,
          hasScrollDownAction: false,
          hasIncreaseAction: false,
          hasDecreaseAction: false,
          hasShowOnScreenAction: false,
          hasMoveCursorForwardByCharacterAction: false,
          hasMoveCursorBackwardByCharacterAction: false,
          hasMoveCursorForwardByWordAction: false,
          hasMoveCursorBackwardByWordAction: false,
          hasSetTextAction: false,
          hasSetSelectionAction: false,
          hasCopyAction: false,
          hasCutAction: false,
          hasPasteAction: false,
          hasDidGainAccessibilityFocusAction: false,
          hasDidLoseAccessibilityFocusAction: false,
          hasDismissAction: false,
        ),
      );
    });

    testWidgets('only matches given flags and actions', (WidgetTester tester) async {
      int allActions = 0;
      int allFlags = 0;
      for (final SemanticsAction action in SemanticsAction.values) {
        allActions |= action.index;
      }
      for (final SemanticsFlag flag in SemanticsFlag.values) {
        allFlags |= flag.index;
      }
      final SemanticsData emptyData = SemanticsData(
        flags: 0,
        actions: 0,
        attributedLabel: AttributedString('a'),
        attributedIncreasedValue: AttributedString('b'),
        attributedValue: AttributedString('c'),
        attributedDecreasedValue: AttributedString('d'),
        attributedHint: AttributedString('e'),
        tooltip: 'f',
        textDirection: TextDirection.ltr,
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        elevation: 3.0,
        thickness: 4.0,
        textSelection: null,
        scrollIndex: null,
        scrollChildCount: null,
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
        platformViewId: 105,
        currentValueLength: 10,
        maxValueLength: 15,
      );
      final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData);

      const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
      final SemanticsData fullData = SemanticsData(
        flags: allFlags,
        actions: allActions,
        attributedLabel: AttributedString('a'),
        attributedIncreasedValue: AttributedString('b'),
        attributedValue: AttributedString('c'),
        attributedDecreasedValue: AttributedString('d'),
        attributedHint: AttributedString('e'),
        tooltip: 'f',
        textDirection: TextDirection.ltr,
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        elevation: 3.0,
        thickness: 4.0,
        textSelection: null,
        scrollIndex: null,
        scrollChildCount: null,
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
        platformViewId: 105,
        currentValueLength: 10,
        maxValueLength: 15,
        customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
      );
      final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData);

      expect(
        emptyNode,
        containsSemantics(
          rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
          size: const Size(10.0, 10.0),
          elevation: 3.0,
          thickness: 4.0,
          platformViewId: 105,
          currentValueLength: 10,
          maxValueLength: 15,
        ),
      );

      expect(
        fullNode,
        containsSemantics(
          rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
          size: const Size(10.0, 10.0),
          elevation: 3.0,
          thickness: 4.0,
          platformViewId: 105,
          currentValueLength: 10,
          maxValueLength: 15,
          customActions: <CustomSemanticsAction>[action],
        ),
      );
    });

    testWidgets('can match child semantics', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();
      const Key key = Key('a');
      await tester.pumpWidget(Semantics(
        key: key,
        label: 'Foo',
        container: true,
        explicitChildNodes: true,
        textDirection: TextDirection.ltr,
        child: Semantics(
          label: 'Bar',
          textDirection: TextDirection.ltr,
        ),
      ));
      final SemanticsNode node = tester.getSemantics(find.byKey(key));

      expect(
        node,
        containsSemantics(
          label: 'Foo',
          textDirection: TextDirection.ltr,
          children: <Matcher>[
            containsSemantics(
              label: 'Bar',
              textDirection: TextDirection.ltr,
            ),
          ],
        ),
      );

      handle.dispose();
    });

    testWidgets('can match only custom actions', (WidgetTester tester) async {
      const CustomSemanticsAction action = CustomSemanticsAction(label: 'test');
      final SemanticsData data = SemanticsData(
        flags: 0,
        actions: SemanticsAction.customAction.index,
        attributedLabel: AttributedString('a'),
        attributedIncreasedValue: AttributedString('b'),
        attributedValue: AttributedString('c'),
        attributedDecreasedValue: AttributedString('d'),
        attributedHint: AttributedString('e'),
        tooltip: 'f',
        textDirection: TextDirection.ltr,
        rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
        elevation: 3.0,
        thickness: 4.0,
        textSelection: null,
        scrollIndex: null,
        scrollChildCount: null,
        scrollPosition: null,
        scrollExtentMax: null,
        scrollExtentMin: null,
        platformViewId: 105,
        currentValueLength: 10,
        maxValueLength: 15,
        customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
      );
      final _FakeSemanticsNode node = _FakeSemanticsNode(data);

      expect(node, containsSemantics(customActions: <CustomSemanticsAction>[action]));
    });

    testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async {
      final SemanticsHandle handle = tester.ensureSemantics();

      const Key key = Key('semantics');
      await tester.pumpWidget(Semantics(
        key: key,
        namesRoute: true,
        header: true,
        button: true,
        link: true,
        onTap: () { },
        onLongPress: () { },
        label: 'foo',
        hint: 'bar',
        value: 'baz',
        increasedValue: 'a',
        decreasedValue: 'b',
        textDirection: TextDirection.rtl,
        onTapHint: 'scan',
        onLongPressHint: 'fill',
        customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
          const CustomSemanticsAction(label: 'foo'): () { },
          const CustomSemanticsAction(label: 'bar'): () { },
        },
      ));

      // This should fail due to the mis-match between the `namesRoute` value.
      void failedExpectation() => expect(tester.getSemantics(find.byKey(key)),
        containsSemantics(
          label: 'foo',
          hint: 'bar',
          value: 'baz',
          increasedValue: 'a',
          decreasedValue: 'b',
          textDirection: TextDirection.rtl,
          hasTapAction: true,
          hasLongPressAction: true,
          isButton: true,
          isLink: true,
          isHeader: true,
          namesRoute: false,
          onTapHint: 'scan',
          onLongPressHint: 'fill',
          customActions: <CustomSemanticsAction>[
            const CustomSemanticsAction(label: 'foo'),
            const CustomSemanticsAction(label: 'bar'),
          ],
        ),
      );

      expect(failedExpectation, throwsA(isA<TestFailure>()));
      handle.dispose();
    });
  });

  group('findsAtLeastNWidgets', () {
    Widget boilerplate(Widget child) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: child,
      );
    }

    testWidgets('succeeds when finds more then the specified count',
        (WidgetTester tester) async {
      await tester.pumpWidget(boilerplate(const Column(
        children: <Widget>[Text('1'), Text('2'), Text('3')],
      )));

      expect(find.byType(Text), findsAtLeastNWidgets(2));
    });

    testWidgets('succeeds when finds the exact specified count',
        (WidgetTester tester) async {
      await tester.pumpWidget(boilerplate(const Column(
        children: <Widget>[Text('1'), Text('2')],
      )));

      expect(find.byType(Text), findsAtLeastNWidgets(2));
    });

    testWidgets('fails when finds less then specified count',
        (WidgetTester tester) async {
      await tester.pumpWidget(boilerplate(const Column(
        children: <Widget>[Text('1'), Text('2')],
      )));

      expect(find.byType(Text), isNot(findsAtLeastNWidgets(3)));
    });
  });

  group('findsOneWidget', () {
    testWidgets('finds exactly one widget', (WidgetTester tester) async {
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));
      expect(find.text('foo'), findsOneWidget);
    });

    testWidgets('fails with a descriptive message', (WidgetTester tester) async {
      late TestFailure failure;
      try {
        expect(find.text('foo', skipOffstage: false), findsOneWidget);
      } on TestFailure catch (e) {
        failure = e;
      }

      expect(failure, isNotNull);
      final String? message = failure.message;
      expect(message, contains('Expected: exactly one matching candidate\n'));
      expect(message, contains('Actual: _TextWidgetFinder:<Found 0 widgets with text "foo"'));
      expect(message, contains('Which: means none were found but one was expected\n'));
    });
  });

  group('findsNothing', () {
    testWidgets('finds no widgets', (WidgetTester tester) async {
      expect(find.text('foo'), findsNothing);
    });

    testWidgets('fails with a descriptive message', (WidgetTester tester) async {
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));

      late TestFailure failure;
      try {
        expect(find.text('foo', skipOffstage: false), findsNothing);
      } on TestFailure catch (e) {
        failure = e;
      }

      expect(failure, isNotNull);
      final String? message = failure.message;

      expect(message, contains('Expected: no matching candidates\n'));
      expect(message, contains('Actual: _TextWidgetFinder:<Found 1 widget with text "foo"'));
      expect(message, contains('Text("foo", textDirection: ltr, dependencies: [MediaQuery])'));
      expect(message, contains('Which: means one was found but none were expected\n'));
    });

    testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async {
      await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr));

      late TestFailure failure;
      try {
        expect(find.text('foo'), findsNothing);
      } on TestFailure catch (e) {
        failure = e;
      }

      expect(failure, isNotNull);
      final String? message = failure.message;

      expect(message, contains('Expected: no matching candidates\n'));
      expect(message, contains('Actual: _TextWidgetFinder:<Found 1 widget with text "foo"'));
      expect(message, contains('Text("foo", textDirection: ltr, dependencies: [MediaQuery])'));
      expect(message, contains('Which: means one was found but none were expected\n'));
    });
  });
}

enum _ComparatorBehavior {
  returnTrue,
  returnFalse,
  throwTestFailure,
}

enum _ComparatorInvocation {
  compare,
  update,
}

class _FakeComparator implements GoldenFileComparator {
  _ComparatorBehavior behavior = _ComparatorBehavior.returnTrue;
  _ComparatorInvocation? invocation;
  Uint8List? imageBytes;
  Uri? golden;

  @override
  Future<bool> compare(Uint8List imageBytes, Uri golden) {
    invocation = _ComparatorInvocation.compare;
    this.imageBytes = imageBytes;
    this.golden = golden;
    switch (behavior) {
      case _ComparatorBehavior.returnTrue:
        return Future<bool>.value(true);
      case _ComparatorBehavior.returnFalse:
        return Future<bool>.value(false);
      case _ComparatorBehavior.throwTestFailure:
        fail('fake message');
    }
  }

  @override
  Future<void> update(Uri golden, Uint8List imageBytes) {
    invocation = _ComparatorInvocation.update;
    this.golden = golden;
    this.imageBytes = imageBytes;
    return Future<void>.value();
  }

  @override
  Uri getTestUri(Uri key, int? version) {
    return key;
  }
}

class _FakeSemanticsNode extends SemanticsNode {
  _FakeSemanticsNode(this.data);

  SemanticsData data;
  @override
  SemanticsData getSemanticsData() => data;
}

@immutable
class _CustomColor extends Color {
  const _CustomColor(super.value, {this.isEqual});
  final bool? isEqual;

  @override
  bool operator ==(Object other) => isEqual ?? super == other;

  @override
  int get hashCode => Object.hash(super.hashCode, isEqual);
}
