// 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:async';

import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';

const DismissDirection defaultDismissDirection = DismissDirection.horizontal;
const double crossAxisEndOffset = 0.5;
bool reportedDismissUpdateReached = false;
bool reportedDismissUpdatePreviousReached = false;
double reportedDismissUpdateProgress = 0.0;
late DismissDirection reportedDismissUpdateReachedDirection;

DismissDirection reportedDismissDirection = DismissDirection.horizontal;
List<int> dismissedItems = <int>[];

Widget buildTest({
  Axis scrollDirection = Axis.vertical,
  DismissDirection dismissDirection = defaultDismissDirection,
  double? startToEndThreshold,
  TextDirection textDirection = TextDirection.ltr,
  Future<bool?> Function(BuildContext context, DismissDirection direction)? confirmDismiss,
  ScrollController? controller,
  ScrollPhysics? scrollPhysics,
  Widget? background,
}) {
  return Directionality(
    textDirection: textDirection,
    child: StatefulBuilder(
      builder: (BuildContext context, StateSetter setState) {
        Widget buildDismissibleItem(int item) {
          return Dismissible(
            dragStartBehavior: DragStartBehavior.down,
            key: ValueKey<int>(item),
            direction: dismissDirection,
            confirmDismiss: confirmDismiss == null ? null : (DismissDirection direction) {
              return confirmDismiss(context, direction);
            },
            onDismissed: (DismissDirection direction) {
              setState(() {
                reportedDismissDirection = direction;
                expect(dismissedItems.contains(item), isFalse);
                dismissedItems.add(item);
              });
            },
            onResize: () {
              expect(dismissedItems.contains(item), isFalse);
            },
            onUpdate: (DismissUpdateDetails details) {
              reportedDismissUpdateReachedDirection = details.direction;
              reportedDismissUpdateReached = details.reached;
              reportedDismissUpdatePreviousReached = details.previousReached;
              reportedDismissUpdateProgress = details.progress;
            },
            background: background,
            dismissThresholds: startToEndThreshold == null
                ? <DismissDirection, double>{}
                : <DismissDirection, double>{DismissDirection.startToEnd: startToEndThreshold},
            crossAxisEndOffset: crossAxisEndOffset,
            child: SizedBox(
              width: 100.0,
              height: 100.0,
              child: Text(item.toString()),
            ),
          );
        }

        return Container(
          padding: const EdgeInsets.all(10.0),
          child: ListView(
            physics: scrollPhysics,
            controller: controller,
            dragStartBehavior: DragStartBehavior.down,
            scrollDirection: scrollDirection,
            itemExtent: 100.0,
            children: <int>[0, 1, 2, 3, 4, 5, 6, 7, 8]
              .where((int i) => !dismissedItems.contains(i))
              .map<Widget>(buildDismissibleItem).toList(),
          ),
        );
      },
    ),
  );
}

typedef DismissMethod = Future<void> Function(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection });

Future<void> dismissElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection }) async {
  Offset downLocation;
  Offset upLocation;
  switch (gestureDirection) {
    case AxisDirection.left:
      // getTopRight() returns a point that's just beyond itemWidget's right
      // edge and outside the Dismissible event listener's bounds.
      downLocation = tester.getTopRight(finder) + const Offset(-0.1, 0.0);
      upLocation = tester.getTopLeft(finder) + const Offset(-0.1, 0.0);
    case AxisDirection.right:
      // we do the same thing here to keep the test symmetric
      downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
      upLocation = tester.getTopRight(finder) + const Offset(0.1, 0.0);
    case AxisDirection.up:
      // getBottomLeft() returns a point that's just below itemWidget's bottom
      // edge and outside the Dismissible event listener's bounds.
      downLocation = tester.getBottomLeft(finder) + const Offset(0.0, -0.1);
      upLocation = tester.getTopLeft(finder) + const Offset(0.0, -0.1);
    case AxisDirection.down:
      // again with doing the same here for symmetry
      downLocation = tester.getTopLeft(finder) + const Offset(0.1, 0.0);
      upLocation = tester.getBottomLeft(finder) + const Offset(0.1, 0.0);
  }

  final TestGesture gesture = await tester.startGesture(downLocation);
  await gesture.moveTo(upLocation);
  await gesture.up();
}

Future<void> dragElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection, required double amount }) async {
  Offset delta;
  switch (gestureDirection) {
    case AxisDirection.left:
      delta = Offset(-amount, 0.0);
    case AxisDirection.right:
      delta = Offset(amount, 0.0);
    case AxisDirection.up:
      delta = Offset(0.0, -amount);
    case AxisDirection.down:
      delta = Offset(0.0, amount);
  }
  await tester.drag(finder, delta);
}

Future<void> flingElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection, double initialOffsetFactor = 0.0 }) async {
  Offset delta;
  switch (gestureDirection) {
    case AxisDirection.left:
      delta = const Offset(-300.0, 0.0);
    case AxisDirection.right:
      delta = const Offset(300.0, 0.0);
    case AxisDirection.up:
      delta = const Offset(0.0, -300.0);
    case AxisDirection.down:
      delta = const Offset(0.0, 300.0);
  }
  await tester.fling(finder, delta, 1000.0, initialOffset: delta * initialOffsetFactor);
}

Future<void> flingElementFromZero(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection }) async {
  // This is a special case where we drag in one direction, then fling back so
  // that at the point of release, we're at exactly the point at which we
  // started, but with velocity. This is needed to check a boundary condition
  // in the flinging behavior.
  await flingElement(tester, finder, gestureDirection: gestureDirection, initialOffsetFactor: -1.0);
}

Future<void> dismissItem(
  WidgetTester tester,
  int item, {
  required AxisDirection gestureDirection,
  DismissMethod mechanism = dismissElement,
}) async {
  final Finder itemFinder = find.text(item.toString());
  expect(itemFinder, findsOneWidget);

  await mechanism(tester, itemFinder, gestureDirection: gestureDirection);
  await tester.pumpAndSettle();
}

Future<void> dragItem(
    WidgetTester tester,
    int item, {
      required AxisDirection gestureDirection,
      required double amount,
    }) async {
  final Finder itemFinder = find.text(item.toString());
  expect(itemFinder, findsOneWidget);

  await dragElement(tester, itemFinder, gestureDirection: gestureDirection, amount: amount);
  await tester.pump();
}

Future<void> checkFlingItemBeforeMovementEnd(
  WidgetTester tester,
  int item, {
  required AxisDirection gestureDirection,
  DismissMethod mechanism = rollbackElement,
}) async {
  final Finder itemFinder = find.text(item.toString());
  expect(itemFinder, findsOneWidget);

  await mechanism(tester, itemFinder, gestureDirection: gestureDirection);

  await tester.pump(); // start the slide
  await tester.pump(const Duration(milliseconds: 100));
}

Future<void> checkFlingItemAfterMovement(
  WidgetTester tester,
  int item, {
  required AxisDirection gestureDirection,
  DismissMethod mechanism = rollbackElement,
}) async {
  final Finder itemFinder = find.text(item.toString());
  expect(itemFinder, findsOneWidget);

  await mechanism(tester, itemFinder, gestureDirection: gestureDirection);

  await tester.pump(); // start the slide
  await tester.pump(const Duration(milliseconds: 300));
}

Future<void> rollbackElement(WidgetTester tester, Finder finder, { required AxisDirection gestureDirection, double initialOffsetFactor = 0.0 }) async {
  Offset delta;
  switch (gestureDirection) {
    case AxisDirection.left:
      delta = const Offset(-30.0, 0.0);
    case AxisDirection.right:
      delta = const Offset(30.0, 0.0);
    case AxisDirection.up:
      delta = const Offset(0.0, -30.0);
    case AxisDirection.down:
      delta = const Offset(0.0, 30.0);
  }
  await tester.fling(finder, delta, 1000.0, initialOffset: delta * initialOffsetFactor);
}

class Test1215DismissibleWidget extends StatelessWidget {
  const Test1215DismissibleWidget(this.text, { super.key });

  final String text;

  @override
  Widget build(BuildContext context) {
    return Dismissible(
      dragStartBehavior: DragStartBehavior.down,
      key: ObjectKey(text),
      child: AspectRatio(
        aspectRatio: 1.0,
        child: Text(text),
      ),
    );
  }
}

void main() {
  setUp(() {
    // Reset "results" variables.
    reportedDismissDirection = defaultDismissDirection;
    dismissedItems = <int>[];
  });

  testWidgetsWithLeakTracking('Horizontal drag triggers dismiss scrollDirection=vertical', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    expect(reportedDismissDirection, DismissDirection.startToEnd);

    await dismissItem(tester, 1, gestureDirection: AxisDirection.left);
    expect(find.text('1'), findsNothing);
    expect(dismissedItems, equals(<int>[0, 1]));
    expect(reportedDismissDirection, DismissDirection.endToStart);
  });

  testWidgetsWithLeakTracking('Horizontal fling triggers dismiss scrollDirection=vertical', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    expect(reportedDismissDirection, DismissDirection.startToEnd);

    await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
    expect(find.text('1'), findsNothing);
    expect(dismissedItems, equals(<int>[0, 1]));
    expect(reportedDismissDirection, DismissDirection.endToStart);
  });

  testWidgetsWithLeakTracking('Horizontal fling does not trigger at zero offset, but does otherwise', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        startToEndThreshold: 0.95,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElementFromZero);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, equals(<int>[]));

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left, mechanism: flingElementFromZero);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, equals(<int>[]));

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    expect(reportedDismissDirection, DismissDirection.startToEnd);

    await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
    expect(find.text('1'), findsNothing);
    expect(dismissedItems, equals(<int>[0, 1]));
    expect(reportedDismissDirection, DismissDirection.endToStart);
  });

  testWidgetsWithLeakTracking('Vertical drag triggers dismiss scrollDirection=horizontal', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        scrollDirection: Axis.horizontal,
        dismissDirection: DismissDirection.vertical,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    expect(reportedDismissDirection, DismissDirection.up);

    await dismissItem(tester, 1, gestureDirection: AxisDirection.down);
    expect(find.text('1'), findsNothing);
    expect(dismissedItems, equals(<int>[0, 1]));
    expect(reportedDismissDirection, DismissDirection.down);
  });

  testWidgetsWithLeakTracking('drag-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        dismissDirection: DismissDirection.endToStart,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);
    await dismissItem(tester, 1, gestureDirection: AxisDirection.right);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    await dismissItem(tester, 1, gestureDirection: AxisDirection.left);
  });

  testWidgetsWithLeakTracking('drag-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        dismissDirection: DismissDirection.startToEnd,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('drag-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        textDirection: TextDirection.rtl,
        dismissDirection: DismissDirection.endToStart,
      ),
    );

    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('drag-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        textDirection: TextDirection.rtl,
        dismissDirection: DismissDirection.startToEnd,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);
    await dismissItem(tester, 1, gestureDirection: AxisDirection.right);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    await dismissItem(tester, 1, gestureDirection: AxisDirection.left);
  });

  testWidgetsWithLeakTracking('fling-left with DismissDirection.endToStart triggers dismiss (LTR)', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        dismissDirection: DismissDirection.endToStart,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);
    await dismissItem(tester, 1, gestureDirection: AxisDirection.right);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    await dismissItem(tester, 1, gestureDirection: AxisDirection.left);
  });

  testWidgetsWithLeakTracking('fling-right with DismissDirection.startToEnd triggers dismiss (LTR)', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        dismissDirection: DismissDirection.startToEnd,
      ),
    );

    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('fling-right with DismissDirection.endToStart triggers dismiss (RTL)', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        textDirection: TextDirection.rtl,
        dismissDirection: DismissDirection.endToStart,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('fling-left with DismissDirection.startToEnd triggers dismiss (RTL)', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        textDirection: TextDirection.rtl,
        dismissDirection: DismissDirection.startToEnd,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);
    await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.right);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.left);
  });

  testWidgetsWithLeakTracking('drag-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        scrollDirection: Axis.horizontal,
        dismissDirection: DismissDirection.up,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('drag-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        scrollDirection: Axis.horizontal,
        dismissDirection: DismissDirection.down,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('fling-up with DismissDirection.up triggers dismiss', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        scrollDirection: Axis.horizontal,
        dismissDirection: DismissDirection.up,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.down);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.up);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('fling-down with DismissDirection.down triggers dismiss', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        scrollDirection: Axis.horizontal,
        dismissDirection: DismissDirection.down,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.up);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.down);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('drag-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        startToEndThreshold: 1.0,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  testWidgetsWithLeakTracking('fling-left has no effect on dismissible with a high dismiss threshold', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        startToEndThreshold: 1.0,
      ),
    );
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
  });

  // This is a regression test for an fn2 bug where dragging a card caused an
  // assert "'!_disqualifiedFromEverAppearingAgain' is not true". The old URL
  // was https://github.com/domokit/sky_engine/issues/1068 but that issue is 404
  // now since we migrated to the new repo. The bug was fixed by
  // https://github.com/flutter/engine/pull/1134 at the time, and later made
  // irrelevant by fn3, but just in case...
  testWidgetsWithLeakTracking('Verify that drag-move events do not assert', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        scrollDirection: Axis.horizontal,
        dismissDirection: DismissDirection.down,
      ),
    );
    final Offset location = tester.getTopLeft(find.text('0'));
    const Offset offset = Offset(0.0, 5.0);
    final TestGesture gesture = await tester.startGesture(location, pointer: 5);
    await gesture.moveBy(offset);
    await tester.pumpWidget(buildTest());
    await gesture.moveBy(offset);
    await tester.pumpWidget(buildTest());
    await gesture.moveBy(offset);
    await tester.pumpWidget(buildTest());
    await gesture.moveBy(offset);
    await tester.pumpWidget(buildTest());
    await gesture.up();
  });

  // This one is for a case where dismissing a widget above a previously
  // dismissed widget threw an exception, which was documented at the
  // now-obsolete URL https://github.com/flutter/engine/issues/1215 (the URL
  // died in the migration to the new repo). Don't copy this test; it doesn't
  // actually remove the dismissed widget, which is a violation of the
  // Dismissible contract. This is not an example of good practice.
  testWidgetsWithLeakTracking('dismissing bottom then top (smoketest)', (WidgetTester tester) async {
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: Center(
          child: SizedBox(
            width: 100.0,
            height: 1000.0,
            child: Column(
              children: <Widget>[
                Test1215DismissibleWidget('1'),
                Test1215DismissibleWidget('2'),
              ],
            ),
          ),
        ),
      ),
    );
    expect(find.text('1'), findsOneWidget);
    expect(find.text('2'), findsOneWidget);
    await dismissElement(tester, find.text('2'), gestureDirection: AxisDirection.right);
    await tester.pump(); // start the slide away
    await tester.pump(const Duration(seconds: 1)); // finish the slide away
    expect(find.text('1'), findsOneWidget);
    expect(find.text('2'), findsNothing);
    await dismissElement(tester, find.text('1'), gestureDirection: AxisDirection.right);
    await tester.pump(); // start the slide away
    await tester.pump(const Duration(seconds: 1)); // finish the slide away (at which point the child is no longer included in the tree)
    expect(find.text('1'), findsNothing);
    expect(find.text('2'), findsNothing);
  });

  testWidgetsWithLeakTracking('Dismissible starts from the full size when collapsing', (WidgetTester tester) async {
    await tester.pumpWidget(
      buildTest(
        background: const Text('background'),
      ),
    );
    expect(dismissedItems, isEmpty);

    final Finder itemFinder = find.text('0');
    expect(itemFinder, findsOneWidget);
    await dismissElement(tester, itemFinder, gestureDirection: AxisDirection.right);
    await tester.pump();

    expect(find.text('background'), findsOneWidget); // The other four have been culled.
    final RenderBox backgroundBox = tester.firstRenderObject(find.text('background'));
    expect(backgroundBox.size.height, equals(100.0));
  });

  testWidgetsWithLeakTracking('Checking fling item before movementDuration completes', (WidgetTester tester) async {
    await tester.pumpWidget(buildTest());
    expect(dismissedItems, isEmpty);

    await checkFlingItemBeforeMovementEnd(tester, 0, gestureDirection: AxisDirection.left, mechanism: flingElement);
    expect(find.text('0'), findsOneWidget);

    await checkFlingItemBeforeMovementEnd(tester, 1, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('1'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('Checking fling item after movementDuration', (WidgetTester tester) async {
    await tester.pumpWidget(buildTest());
    expect(dismissedItems, isEmpty);

    await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
    expect(find.text('1'), findsNothing);

    await checkFlingItemAfterMovement(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('0'), findsNothing);
  });

  testWidgetsWithLeakTracking('Horizontal fling less than threshold', (WidgetTester tester) async {
    await tester.pumpWidget(buildTest(scrollDirection: Axis.horizontal));
    expect(dismissedItems, isEmpty);

    await checkFlingItemAfterMovement(tester, 0, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.right);
    expect(find.text('1'), findsOneWidget);
    expect(dismissedItems, isEmpty);
  });

  testWidgetsWithLeakTracking('Vertical fling less than threshold', (WidgetTester tester) async {
    await tester.pumpWidget(buildTest());
    expect(dismissedItems, isEmpty);

    await checkFlingItemAfterMovement(tester, 0, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.right);
    expect(find.text('1'), findsOneWidget);
    expect(dismissedItems, isEmpty);
  });

  testWidgetsWithLeakTracking('confirmDismiss returns values: true, false, null', (WidgetTester tester) async {
    late DismissDirection confirmDismissDirection;

    Widget buildFrame(bool? confirmDismissValue) {
      return buildTest(
          confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
          confirmDismissDirection = dismissDirection;
          return Future<bool?>.value(confirmDismissValue);
        },
      );
    }

    // Dismiss is confirmed IFF confirmDismiss() returns true.
    await tester.pumpWidget(buildFrame(true));
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    expect(reportedDismissDirection, DismissDirection.startToEnd);
    expect(confirmDismissDirection, DismissDirection.startToEnd);

    await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
    expect(find.text('1'), findsNothing);
    expect(dismissedItems, equals(<int>[0, 1]));
    expect(reportedDismissDirection, DismissDirection.endToStart);
    expect(confirmDismissDirection, DismissDirection.endToStart);

    // Dismiss is not confirmed if confirmDismiss() returns false
    dismissedItems = <int>[];
    await tester.pumpWidget(buildFrame(false));

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);
    expect(confirmDismissDirection, DismissDirection.startToEnd);

    await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
    expect(find.text('1'), findsOneWidget);
    expect(dismissedItems, isEmpty);
    expect(confirmDismissDirection, DismissDirection.endToStart);

    // Dismiss is not confirmed if confirmDismiss() returns null
    dismissedItems = <int>[];
    await tester.pumpWidget(buildFrame(null));

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);
    expect(confirmDismissDirection, DismissDirection.startToEnd);

    await dismissItem(tester, 1, gestureDirection: AxisDirection.left, mechanism: flingElement);
    expect(find.text('1'), findsOneWidget);
    expect(dismissedItems, isEmpty);
    expect(confirmDismissDirection, DismissDirection.endToStart);
  });

  testWidgetsWithLeakTracking('Pending confirmDismiss does not cause errors', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/54990

    late Completer<bool?> completer;
    Widget buildFrame() {
      completer = Completer<bool?>();
      return buildTest(
        confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
          return completer.future;
        },
      );
    }

    // false for _handleDragEnd - when dragged to the end and released

    await tester.pumpWidget(buildFrame());

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await tester.pumpWidget(const SizedBox());
    completer.complete(false);
    await tester.pump();

    // true for _handleDragEnd - when dragged to the end and released

    await tester.pumpWidget(buildFrame());

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await tester.pumpWidget(const SizedBox());
    completer.complete(true);
    await tester.pump();

    // false for _handleDismissStatusChanged - when fling reaches the end

    await tester.pumpWidget(buildFrame());

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await tester.pumpWidget(const SizedBox());
    completer.complete(false);
    await tester.pump();

    // true for _handleDismissStatusChanged - when fling reaches the end

    await tester.pumpWidget(buildFrame());

    await dismissItem(tester, 0, gestureDirection: AxisDirection.right, mechanism: flingElement);
    expect(find.text('0'), findsOneWidget);
    expect(dismissedItems, isEmpty);

    await tester.pumpWidget(const SizedBox());
    completer.complete(true);
    await tester.pump();
  });

  testWidgetsWithLeakTracking('Dismissible cannot be dragged with pending confirmDismiss', (WidgetTester tester) async {
    final Completer<bool?> completer = Completer<bool?>();
    await tester.pumpWidget(
      buildTest(
        confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
          return completer.future;
        },
      ),
    );

    // Trigger confirmDismiss call.
    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    final Offset position = tester.getTopLeft(find.text('0'));

    // Try to move and verify it has not moved.
    Offset dragAt = tester.getTopLeft(find.text('0'));
    dragAt = Offset(100.0, dragAt.dy);
    final TestGesture gesture = await tester.startGesture(dragAt);
    await gesture.moveTo(dragAt + const Offset(100.0, 0.0));
    await gesture.up();
    await tester.pump();
    expect(tester.getTopLeft(find.text('0')), position);
  });

  testWidgetsWithLeakTracking('Drag to end and release - items does not get stuck if confirmDismiss returns false', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/87556

    final Completer<bool?> completer = Completer<bool?>();
    await tester.pumpWidget(
      buildTest(
        confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
          return completer.future;
        },
      ),
    );

    final Offset position = tester.getTopLeft(find.text('0'));
    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    completer.complete(false);
    await tester.pumpAndSettle();
    expect(tester.getTopLeft(find.text('0')), position);
  });

  testWidgetsWithLeakTracking('Dismissible with null resizeDuration calls onDismissed immediately', (WidgetTester tester) async {
    bool resized = false;
    bool dismissed = false;

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Dismissible(
          dragStartBehavior: DragStartBehavior.down,
          key: UniqueKey(),
          resizeDuration: null,
          onDismissed: (DismissDirection direction) {
            dismissed = true;
          },
          onResize: () {
            resized = true;
          },
          child: const SizedBox(
            width: 100.0,
            height: 100.0,
            child: Text('0'),
          ),
        ),
      ),
    );

    await dismissElement(tester, find.text('0'), gestureDirection: AxisDirection.right);
    await tester.pump();
    expect(dismissed, true);
    expect(resized, false);
  });

  testWidgetsWithLeakTracking('setState that does not remove the Dismissible from tree should throw Error', (WidgetTester tester) async {
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: StatefulBuilder(
        builder: (BuildContext context, StateSetter setState) {
          return ListView(
            dragStartBehavior: DragStartBehavior.down,
            itemExtent: 100.0,
            children: <Widget>[
              Dismissible(
                dragStartBehavior: DragStartBehavior.down,
                key: const ValueKey<int>(1),
                onDismissed: (DismissDirection direction) {
                  setState(() {
                    reportedDismissDirection = direction;
                    expect(dismissedItems.contains(1), isFalse);
                    dismissedItems.add(1);
                  });
                },
                crossAxisEndOffset: crossAxisEndOffset,
                child: SizedBox(
                  width: 100.0,
                  height: 100.0,
                  child: Text(1.toString()),
                ),
              ),
            ],
          );
        },
      ),
    ));
    expect(dismissedItems, isEmpty);
    await dismissItem(tester, 1, gestureDirection: AxisDirection.right);
    expect(dismissedItems, equals(<int>[1]));
    final dynamic exception =  tester.takeException();
    expect(exception, isNotNull);
    expect(exception, isFlutterError);
    final FlutterError error = exception as FlutterError;
    expect(error.diagnostics.last.level, DiagnosticLevel.hint);
    expect(
      error.diagnostics.last.toStringDeep(),
      equalsIgnoringHashCodes(
        'Make sure to implement the onDismissed handler and to immediately\n'
        'remove the Dismissible widget from the application once that\n'
        'handler has fired.\n',
      ),
    );
    expect(
      error.toStringDeep(),
      'FlutterError\n'
      '   A dismissed Dismissible widget is still part of the tree.\n'
      '   Make sure to implement the onDismissed handler and to immediately\n'
      '   remove the Dismissible widget from the application once that\n'
      '   handler has fired.\n',
    );
  });

  testWidgetsWithLeakTracking('Dismissible.behavior should behave correctly during hit testing', (WidgetTester tester) async {
    bool didReceivePointerDown = false;

    Widget buildStack({required Widget child}) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
          children: <Widget>[
            Listener(
              onPointerDown: (_) {
                didReceivePointerDown = true;
              },
              child: Container(
                width: 100.0,
                height: 100.0,
                color: const Color(0xFF00FF00),
              ),
            ),
            child,
          ],
        ),
      );
    }

    await tester.pumpWidget(
      buildStack(
        child: const Dismissible(
          key: ValueKey<int>(1),
          child: SizedBox(
            width: 100.0,
            height: 100.0,
          ),
        ),
      ),
    );
    await tester.tapAt(const Offset(10.0, 10.0));
    expect(didReceivePointerDown, isFalse);

    Future<void> pumpWidgetTree(HitTestBehavior behavior) {
      return tester.pumpWidget(
        buildStack(
          child: Dismissible(
            key: const ValueKey<int>(1),
            behavior: behavior,
            child: const SizedBox(
              width: 100.0,
              height: 100.0,
            ),
          ),
        ),
      );
    }

    didReceivePointerDown = false;
    await pumpWidgetTree(HitTestBehavior.deferToChild);
    await tester.tapAt(const Offset(10.0, 10.0));
    expect(didReceivePointerDown, isTrue);

    didReceivePointerDown = false;
    await pumpWidgetTree(HitTestBehavior.opaque);
    await tester.tapAt(const Offset(10.0, 10.0));
    expect(didReceivePointerDown, isFalse);

    didReceivePointerDown = false;
    await pumpWidgetTree(HitTestBehavior.translucent);
    await tester.tapAt(const Offset(10.0, 10.0));
    expect(didReceivePointerDown, isTrue);
  });

  testWidgetsWithLeakTracking('DismissDirection.none does not trigger dismiss', (WidgetTester tester) async {
    await tester.pumpWidget(buildTest(
      dismissDirection: DismissDirection.none,
      scrollPhysics: const NeverScrollableScrollPhysics(),
    ));
    expect(dismissedItems, isEmpty);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
    await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
    expect(find.text('0'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('DismissDirection.none does not prevent scrolling', (WidgetTester tester) async {
    final ScrollController controller = ScrollController();

    await tester.pumpWidget(
      buildTest(
        controller: controller,
        dismissDirection: DismissDirection.none,
      ),
    );
    expect(dismissedItems, isEmpty);
    expect(controller.offset, 0.0);

    await dismissItem(tester, 0, gestureDirection: AxisDirection.left);
    expect(controller.offset, 0.0);
    await dismissItem(tester, 0, gestureDirection: AxisDirection.right);
    expect(controller.offset, 0.0);
    await dismissItem(tester, 0, gestureDirection: AxisDirection.down);
    expect(controller.offset, 0.0);
    await dismissItem(tester, 0, gestureDirection: AxisDirection.up);
    expect(controller.offset, 100.0);
    controller.dispose();
  });

  testWidgetsWithLeakTracking('onUpdate', (WidgetTester tester) async {
    await tester.pumpWidget(buildTest(
      scrollDirection: Axis.horizontal,
    ));
    expect(dismissedItems, isEmpty);

    // Unsuccessful dismiss, fractional progress reported
    await dragItem(tester, 0, gestureDirection: AxisDirection.right, amount: 20);
    expect(reportedDismissUpdateProgress, 0.2);

    // Successful dismiss therefore threshold has been reached
    await dismissItem(tester, 0, mechanism: flingElement, gestureDirection: AxisDirection.left);
    expect(find.text('0'), findsNothing);
    expect(dismissedItems, equals(<int>[0]));
    expect(reportedDismissUpdateReachedDirection, DismissDirection.endToStart);
    expect(reportedDismissUpdateReached, true);
    expect(reportedDismissUpdatePreviousReached, true);
    expect(reportedDismissUpdateProgress, 1.0);

    // Unsuccessful dismiss, threshold has not been reached
    await checkFlingItemAfterMovement(tester, 1, gestureDirection: AxisDirection.right);
    expect(find.text('1'), findsOneWidget);
    expect(dismissedItems, equals(<int>[0]));
    expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
    expect(reportedDismissUpdateReached, false);
    expect(reportedDismissUpdatePreviousReached, false);
    expect(reportedDismissUpdateProgress, 0.0);

    // Another successful dismiss from another direction
    await dismissItem(tester, 1, mechanism: flingElement, gestureDirection: AxisDirection.right);
    expect(find.text('1'), findsNothing);
    expect(dismissedItems, equals(<int>[0, 1]));
    expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
    expect(reportedDismissUpdateReached, true);
    expect(reportedDismissUpdatePreviousReached, true);
    expect(reportedDismissUpdateProgress, 1.0);

    await tester.pumpWidget(buildTest(
      scrollDirection: Axis.horizontal,
      confirmDismiss: (BuildContext context, DismissDirection dismissDirection) {
        return Future<bool>.value(false);
      },
    ));

    // Threshold has been reached but dismiss was not confirmed
    await dismissItem(tester, 2, mechanism: flingElement, gestureDirection: AxisDirection.right);
    expect(find.text('2'), findsOneWidget);
    expect(dismissedItems, equals(<int>[0, 1]));
    expect(reportedDismissUpdateReachedDirection, DismissDirection.startToEnd);
    expect(reportedDismissUpdateReached, false);
    expect(reportedDismissUpdatePreviousReached, false);
    expect(reportedDismissUpdateProgress, 0.0);
  });

  testWidgetsWithLeakTracking('Change direction does not lose child state', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/108961
    Widget buildFrame(DismissDirection direction) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Dismissible(
          dragStartBehavior: DragStartBehavior.down,
          direction: direction,
          key: const Key('Dismissible'),
          resizeDuration: null,
          child: const SizedBox(
            width: 100.0,
            height: 100.0,
            child: Text('I Love Flutter!'),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(DismissDirection.horizontal));
    final RenderBox textRenderObjectBegin = tester.renderObject(find.text('I Love Flutter!'));

    await tester.pumpWidget(buildFrame(DismissDirection.none));
    final RenderBox textRenderObjectEnd = tester.renderObject(find.text('I Love Flutter!'));

    expect(identical(textRenderObjectBegin, textRenderObjectEnd), true);
  });
}