// 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 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

import 'semantics_tester.dart';

void main() {
  testWidgets('SliverReorderableList works well when having gestureSettings', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/103404
    const int itemCount = 5;
    int onReorderCallCount = 0;
    final List<int> items = List<int>.generate(itemCount, (int index) => index);

    void handleReorder(int fromIndex, int toIndex) {
      onReorderCallCount += 1;
      if (toIndex > fromIndex) {
        toIndex -= 1;
      }
      items.insert(toIndex, items.removeAt(fromIndex));
    }
    // The list has five elements of height 100
    await tester.pumpWidget(
      MaterialApp(
        home: MediaQuery(
          data: const MediaQueryData(gestureSettings: DeviceGestureSettings(touchSlop: 8.0)),
          child: CustomScrollView(
            slivers: <Widget>[
              SliverReorderableList(
                itemCount: itemCount,
                itemBuilder: (BuildContext context, int index) {
                  return SizedBox(
                    key: ValueKey<int>(items[index]),
                    height: 100,
                    child: ReorderableDragStartListener(
                      index: index,
                      child: Text('item ${items[index]}'),
                    ),
                  );
                },
                onReorder: handleReorder,
              )
            ],
          ),
        ),
      ),
    );

    // Start gesture on first item
    final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0')));
    await tester.pump(kPressTimeout);

    // Drag a little bit to make `ImmediateMultiDragGestureRecognizer` compete with `VerticalDragGestureRecognizer`
    await drag.moveBy(const Offset(0, 10));
    await tester.pump();
    // Drag enough to move down the first item
    await drag.moveBy(const Offset(0, 40));
    await tester.pump();
    await drag.up();
    await tester.pumpAndSettle();

    expect(onReorderCallCount, 1);
    expect(items, orderedEquals(<int>[1, 0, 2, 3, 4]));
  });

  testWidgets('SliverReorderableList item has correct semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const int itemCount = 5;
    int onReorderCallCount = 0;
    final List<int> items = List<int>.generate(itemCount, (int index) => index);

    void handleReorder(int fromIndex, int toIndex) {
      onReorderCallCount += 1;
      if (toIndex > fromIndex) {
        toIndex -= 1;
      }
      items.insert(toIndex, items.removeAt(fromIndex));
    }
    // The list has five elements of height 100
    await tester.pumpWidget(
      MaterialApp(
        home: MediaQuery(
          data: const MediaQueryData(gestureSettings: DeviceGestureSettings(touchSlop: 8.0)),
          child: CustomScrollView(
            slivers: <Widget>[
              SliverReorderableList(
                itemCount: itemCount,
                itemBuilder: (BuildContext context, int index) {
                  return SizedBox(
                    key: ValueKey<int>(items[index]),
                    height: 100,
                    child: ReorderableDragStartListener(
                      index: index,
                      child: Text('item ${items[index]}'),
                    ),
                  );
                },
                onReorder: handleReorder,
              )
            ],
          ),
        ),
      ),
    );

    expect(
      semantics,
      includesNodeWith(
        label: 'item 0',
        actions: <SemanticsAction>[SemanticsAction.customAction],
      ),
    );
    final SemanticsNode node = tester.getSemantics(find.text('item 0'));

    // perform custom action 'move down'.
    final int customActionId = CustomSemanticsAction.getIdentifier(const CustomSemanticsAction(label: 'Move down'));
    tester.binding.pipelineOwner.semanticsOwner!.performAction(node.id, SemanticsAction.customAction, customActionId);
    await tester.pumpAndSettle();

    expect(onReorderCallCount, 1);
    expect(items, orderedEquals(<int>[1, 0, 2, 3, 4]));

    semantics.dispose();
  });

  testWidgets('SliverReorderableList custom semantics action has correct label', (WidgetTester tester) async {
    const int itemCount = 5;
    final List<int> items = List<int>.generate(itemCount, (int index) => index);
    // The list has five elements of height 100
    await tester.pumpWidget(
      MaterialApp(
        home: MediaQuery(
          data: const MediaQueryData(gestureSettings: DeviceGestureSettings(touchSlop: 8.0)),
          child: CustomScrollView(
            slivers: <Widget>[
              SliverReorderableList(
                itemCount: itemCount,
                itemBuilder: (BuildContext context, int index) {
                  return SizedBox(
                    key: ValueKey<int>(items[index]),
                    height: 100,
                    child: ReorderableDragStartListener(
                      index: index,
                      child: Text('item ${items[index]}'),
                    ),
                  );
                },
                onReorder: (int _, int __) { },
              )
            ],
          ),
        ),
      ),
    );
    final SemanticsNode node = tester.getSemantics(find.text('item 0'));
    final SemanticsData data = node.getSemanticsData();
    expect(data.customSemanticsActionIds!.length, 2);
    final CustomSemanticsAction action1 = CustomSemanticsAction.getAction(data.customSemanticsActionIds![0])!;
    expect(action1.label, 'Move down');
    final CustomSemanticsAction action2 = CustomSemanticsAction.getAction(data.customSemanticsActionIds![1])!;
    expect(action2.label, 'Move to the end');
  });

  // Regression test for https://github.com/flutter/flutter/issues/100451
  testWidgets('SliverReorderableList.builder respects findChildIndexCallback', (WidgetTester tester) async {
    bool finderCalled = false;
    int itemCount = 7;
    late StateSetter stateSetter;

    await tester.pumpWidget(
      MaterialApp(
        home: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            stateSetter = setState;
            return CustomScrollView(
              slivers: <Widget>[
                SliverReorderableList(
                  itemCount: itemCount,
                  itemBuilder: (BuildContext _, int index) => Container(
                    key: Key('$index'),
                    height: 2000.0,
                  ),
                  findChildIndexCallback: (Key key) {
                    finderCalled = true;
                    return null;
                  },
                  onReorder: (int oldIndex, int newIndex) { },
                ),
              ],
            );
          },
        ),
      )
    );
    expect(finderCalled, false);

    // Trigger update.
    stateSetter(() => itemCount = 77);
    await tester.pump();

    expect(finderCalled, true);
  });

  // Regression test for https://github.com/flutter/flutter/issues/88191
  testWidgets('Do not crash when dragging with two fingers simultaneously', (WidgetTester tester) async {
    final List<int> items = List<int>.generate(3, (int index) => index);
    void handleReorder(int fromIndex, int toIndex) {
      if (toIndex > fromIndex) {
        toIndex -= 1;
      }
      items.insert(toIndex, items.removeAt(fromIndex));
    }

    await tester.pumpWidget(MaterialApp(
      home: ReorderableList(
        itemBuilder: (BuildContext context, int index) {
          return ReorderableDragStartListener(
            index: index,
            key: ValueKey<int>(items[index]),
            child: SizedBox(
              height: 100,
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text('item ${items[index]}'),
                ],
              ),
            ),
          );
        },
        itemCount: items.length,
        onReorder: handleReorder,
      ),
    ));

    final TestGesture drag1 = await tester.startGesture(tester.getCenter(find.text('item 0')));
    final TestGesture drag2 = await tester.startGesture(tester.getCenter(find.text('item 0')));
    await tester.pump(kLongPressTimeout);

    await drag1.moveBy(const Offset(0, 100));
    await drag2.moveBy(const Offset(0, 100));
    await tester.pumpAndSettle();

    await drag1.up();
    await drag2.up();
    await tester.pumpAndSettle();

    expect(tester.takeException(), isNull);
  });

  testWidgets('negative itemCount should assert', (WidgetTester tester) async {
    final List<int> items = <int>[1, 2, 3];
    await tester.pumpWidget(MaterialApp(
      home: StatefulBuilder(
        builder: (BuildContext outerContext, StateSetter setState) {
          return CustomScrollView(
            slivers: <Widget>[
              SliverReorderableList(
                itemCount: -1,
                onReorder: (int fromIndex, int toIndex) {
                  setState(() {
                    if (toIndex > fromIndex) {
                      toIndex -= 1;
                    }
                    items.insert(toIndex, items.removeAt(fromIndex));
                  });
                },
                itemBuilder: (BuildContext context, int index) {
                  return SizedBox(
                    height: 100,
                    child: Text('item ${items[index]}'),
                  );
                },
              ),
            ],
          );
        },
      ),
    ));
    expect(tester.takeException(), isA<AssertionError>());
  });

  testWidgets('zero itemCount should not build widget', (WidgetTester tester) async {
    final List<int> items = <int>[1, 2, 3];
    await tester.pumpWidget(MaterialApp(
      home: StatefulBuilder(
        builder: (BuildContext outerContext, StateSetter setState) {
          return CustomScrollView(
            slivers: <Widget>[
              SliverFixedExtentList(
                itemExtent: 50.0,
                delegate: SliverChildListDelegate(<Widget>[
                  const Text('before'),
                ]),
              ),
              SliverReorderableList(
                itemCount: 0,
                onReorder: (int fromIndex, int toIndex) {
                  setState(() {
                    if (toIndex > fromIndex) {
                      toIndex -= 1;
                    }
                    items.insert(toIndex, items.removeAt(fromIndex));
                  });
                },
                itemBuilder: (BuildContext context, int index) {
                  return SizedBox(
                    height: 100,
                    child: Text('item ${items[index]}'),
                  );
                },
              ),
              SliverFixedExtentList(
                itemExtent: 50.0,
                delegate: SliverChildListDelegate(<Widget>[
                  const Text('after'),
                ]),
              ),
            ],
          );
        },
      ),
    ));

    expect(find.text('before'), findsOneWidget);
    expect(find.byType(SliverReorderableList), findsNothing);
    expect(find.text('after'), findsOneWidget);
  });

  testWidgets('SliverReorderableList, drag and drop, fixed height items', (WidgetTester tester) async {
    final List<int> items = List<int>.generate(8, (int index) => index);

    Future<void> pressDragRelease(Offset start, Offset delta) async {
      final TestGesture drag = await tester.startGesture(start);
      await tester.pump(kPressTimeout);
      await drag.moveBy(delta);
      await tester.pump(kPressTimeout);
      await drag.up();
      await tester.pumpAndSettle();
    }

    void check({ List<int> visible = const <int>[], List<int> hidden = const <int>[] }) {
      for (final int i in visible) {
        expect(find.text('item $i'), findsOneWidget);
      }
      for (final int i in hidden) {
        expect(find.text('item $i'), findsNothing);
      }
    }

    // The SliverReorderableList is 800x600, 8 items, each item is 800x100 with
    // an "item $index" text widget at the item's origin.  Drags are initiated by
    // a simple press on the text widget.
    await tester.pumpWidget(TestList(items: items));
    check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[6, 7]);

    // Drag item 0 downwards less than halfway and let it snap back. List
    // should remain as it is.
    await pressDragRelease(const Offset(12, 50), const Offset(12, 60));
    check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[6, 7]);
    expect(tester.getTopLeft(find.text('item 0')), Offset.zero);
    expect(tester.getTopLeft(find.text('item 1')), const Offset(0, 100));
    expect(items, orderedEquals(<int>[0, 1, 2, 3, 4, 5, 6, 7]));

    // Drag item 0 downwards more than halfway to displace item 1.
    await pressDragRelease(tester.getCenter(find.text('item 0')), const Offset(0, 51));
    check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[6, 7]);
    expect(tester.getTopLeft(find.text('item 1')), Offset.zero);
    expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 100));
    expect(items, orderedEquals(<int>[1, 0, 2, 3, 4, 5, 6, 7]));

    // Drag item 0 back to where it was.
    await pressDragRelease(tester.getCenter(find.text('item 0')), const Offset(0, -51));
    check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[6, 7]);
    expect(tester.getTopLeft(find.text('item 0')), Offset.zero);
    expect(tester.getTopLeft(find.text('item 1')), const Offset(0, 100));
    expect(items, orderedEquals(<int>[0, 1, 2, 3, 4, 5, 6, 7]));

    // Drag item 1 to item 3
    await pressDragRelease(tester.getCenter(find.text('item 1')), const Offset(0, 151));
    check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[6, 7]);
    expect(tester.getTopLeft(find.text('item 0')), Offset.zero);
    expect(tester.getTopLeft(find.text('item 1')), const Offset(0, 300));
    expect(tester.getTopLeft(find.text('item 3')), const Offset(0, 200));
    expect(items, orderedEquals(<int>[0, 2, 3, 1, 4, 5, 6, 7]));

    // Drag item 1 back to where it was
    await pressDragRelease(tester.getCenter(find.text('item 1')), const Offset(0, -200));
    check(visible: <int>[0, 1, 2, 3, 4, 5], hidden: <int>[6, 7]);
    expect(tester.getTopLeft(find.text('item 0')), Offset.zero);
    expect(tester.getTopLeft(find.text('item 1')), const Offset(0, 100));
    expect(tester.getTopLeft(find.text('item 3')), const Offset(0, 300));
    expect(items, orderedEquals(<int>[0, 1, 2, 3, 4, 5, 6, 7]));
  });

  testWidgets('SliverReorderableList, items inherit DefaultTextStyle, IconTheme', (WidgetTester tester) async {
    const Color textColor = Color(0xffffffff);
    const Color iconColor = Color(0xff0000ff);

    TextStyle getIconStyle() {
      return tester.widget<RichText>(
        find.descendant(
          of: find.byType(Icon),
          matching: find.byType(RichText),
        ),
      ).text.style!;
    }

    TextStyle getTextStyle() {
      return tester.widget<RichText>(
        find.descendant(
          of: find.text('item 0'),
          matching: find.byType(RichText),
        ),
      ).text.style!;
    }

    // This SliverReorderableList has just one item: "item 0".
    await tester.pumpWidget(
      TestList(
        items: List<int>.from(<int>[0]),
        textColor: textColor,
        iconColor: iconColor,
      ),
    );
    expect(tester.getTopLeft(find.text('item 0')), Offset.zero);
    expect(getIconStyle().color, iconColor);
    expect(getTextStyle().color, textColor);

    // Dragging item 0 causes it to be reparented in the overlay. The item
    // should still inherit the IconTheme and DefaultTextStyle because they are
    // InheritedThemes.
    final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0')));
    await tester.pump(kPressTimeout);
    await drag.moveBy(const Offset(0, 50));
    await tester.pump(kPressTimeout);
    expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 50));
    expect(getIconStyle().color, iconColor);
    expect(getTextStyle().color, textColor);

    // Drag is complete, item 0 returns to where it was.
    await drag.up();
    await tester.pumpAndSettle();
    expect(tester.getTopLeft(find.text('item 0')), Offset.zero);
    expect(getIconStyle().color, iconColor);
    expect(getTextStyle().color, textColor);
  });

  testWidgets('SliverReorderableList - custom proxyDecorator', (WidgetTester tester) async {
    const ValueKey<String> fadeTransitionKey = ValueKey<String>('reordered-fade');

    await tester.pumpWidget(
      TestList(
        items: List<int>.from(<int>[0, 1, 2, 3]),
        proxyDecorator: (
            Widget child,
            int index,
            Animation<double> animation,
            ) {
          return AnimatedBuilder(
            animation: animation,
            builder: (BuildContext context, Widget? child) {
              final Tween<double> fadeValues = Tween<double>(begin: 1.0, end: 0.5);
              final Animation<double> fadeAnimation = animation.drive(fadeValues);
              return FadeTransition(
                key: fadeTransitionKey,
                opacity: fadeAnimation,
                child: child,
              );
            },
            child: child,
          );
        },
      ),
    );

    Finder getItemFadeTransition() => find.byKey(fadeTransitionKey);

    expect(getItemFadeTransition(), findsNothing);

    // Start gesture on first item
    final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0')));
    await tester.pump(kPressTimeout);

    // Drag enough for transition animation defined in proxyDecorator to start.
    await drag.moveBy(const Offset(0, 50));
    await tester.pump();

    // At the start, opacity should be at 1.0.
    expect(getItemFadeTransition(), findsOneWidget);
    FadeTransition fadeTransition = tester.widget(getItemFadeTransition());
    expect(fadeTransition.opacity.value, 1.0);

    // Let animation run halfway.
    await tester.pump(const Duration(milliseconds: 125));
    fadeTransition = tester.widget(getItemFadeTransition());
    expect(fadeTransition.opacity.value, greaterThan(0.5));
    expect(fadeTransition.opacity.value, lessThan(1.0));

    // Allow animation to run to the end.
    await tester.pumpAndSettle();
    expect(find.byKey(fadeTransitionKey), findsOneWidget);
    fadeTransition = tester.widget(getItemFadeTransition());
    expect(fadeTransition.opacity.value, 0.5);

    // Finish reordering.
    await drag.up();
    await tester.pumpAndSettle();
    expect(getItemFadeTransition(), findsNothing);
  });

  testWidgets('ReorderableList supports items with nested list views without throwing layout exception.', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            // Ensure there is always a top padding to simulate a phone with
            // safe area at the top. If the nested list doesn't have the
            // padding removed before it is put into the overlay it will
            // overflow the layout by the top padding.
            data: MediaQuery.of(context).copyWith(padding: const EdgeInsets.only(top: 50)),
            child: child!,
          );
        },
        home: Scaffold(
          appBar: AppBar(title: const Text('Nested Lists')),
          body: ReorderableList(
            itemCount: 10,
            itemBuilder: (BuildContext context, int index) {
              return ReorderableDragStartListener(
                index: index,
                key: ValueKey<int>(index),
                child: Column(
                  children: <Widget>[
                    ListView(
                      shrinkWrap: true,
                      physics: const ClampingScrollPhysics(),
                      children: const <Widget>[
                        Text('Other data'),
                        Text('Other data'),
                        Text('Other data'),
                      ],
                    ),
                  ],
                ),
              );
            },
            onReorder: (int oldIndex, int newIndex) {},
          ),
        ),
      ),
    );

    // Start gesture on first item
    final TestGesture drag = await tester.startGesture(tester.getCenter(find.byKey(const ValueKey<int>(0))));
    await tester.pump(kPressTimeout);

    // Drag enough for move to start
    await drag.moveBy(const Offset(0, 50));
    await tester.pumpAndSettle();

    // There shouldn't be a layout overflow exception.
    expect(tester.takeException(), isNull);
  });

  testWidgets('ReorderableList supports items with nested list views without throwing layout exception.', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/83224.
    await tester.pumpWidget(
      MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            // Ensure there is always a top padding to simulate a phone with
            // safe area at the top. If the nested list doesn't have the
            // padding removed before it is put into the overlay it will
            // overflow the layout by the top padding.
            data: MediaQuery.of(context).copyWith(padding: const EdgeInsets.only(top: 50)),
            child: child!,
          );
        },
        home: Scaffold(
          appBar: AppBar(title: const Text('Nested Lists')),
          body: ReorderableList(
            itemCount: 10,
            itemBuilder: (BuildContext context, int index) {
              return ReorderableDragStartListener(
                index: index,
                key: ValueKey<int>(index),
                child: Column(
                  children: <Widget>[
                    ListView(
                      shrinkWrap: true,
                      physics: const ClampingScrollPhysics(),
                      children: const <Widget>[
                        Text('Other data'),
                        Text('Other data'),
                        Text('Other data'),
                      ],
                    ),
                  ],
                ),
              );
            },
            onReorder: (int oldIndex, int newIndex) {},
          ),
        ),
      ),
    );

    // Start gesture on first item.
    final TestGesture drag = await tester.startGesture(tester.getCenter(find.byKey(const ValueKey<int>(0))));
    await tester.pump(kPressTimeout);

    // Drag enough for move to start.
    await drag.moveBy(const Offset(0, 50));
    await tester.pumpAndSettle();

    // There shouldn't be a layout overflow exception.
    expect(tester.takeException(), isNull);
  });

  testWidgets('SliverReorderableList - properly animates the drop in a reversed list', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/110949
    final List<int> items = List<int>.generate(8, (int index) => index);

    Future<void> pressDragRelease(Offset start, Offset delta) async {
      final TestGesture drag = await tester.startGesture(start);
      await tester.pump(kPressTimeout);
      await drag.moveBy(delta);
      await tester.pumpAndSettle();
      await drag.up();
      await tester.pump();
    }

    // The TestList is 800x600 SliverReorderableList with 8 items 800x100 each.
    // Each item has a text widget with 'item $index' that can be moved by a
    // press and drag gesture. For this test we are reversing the order so
    // the first item is at the bottom.
    await tester.pumpWidget(TestList(items: items, reverse: true));

    expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 500));
    expect(tester.getTopLeft(find.text('item 2')), const Offset(0, 300));

    // Drag item 0 up and insert it between item 1 and item 2. It should
    // smoothly animate.
    await pressDragRelease(tester.getCenter(find.text('item 0')), const Offset(0, -50));
    expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 450));
    expect(tester.getTopLeft(find.text('item 1')), const Offset(0, 500));
    expect(tester.getTopLeft(find.text('item 2')), const Offset(0, 300));

    // After the first several frames we should be moving closer to the final position,
    // not further away as was the case with the original bug.
    await tester.pump(const Duration(milliseconds: 10));
    expect(tester.getTopLeft(find.text('item 0')).dy, lessThan(450));
    expect(tester.getTopLeft(find.text('item 0')).dy, greaterThan(400));

    // Sample the middle (don't use exact values as it depends on the internal
    // curve being used).
    await tester.pump(const Duration(milliseconds: 125));
    expect(tester.getTopLeft(find.text('item 0')).dy, lessThan(450));
    expect(tester.getTopLeft(find.text('item 0')).dy, greaterThan(400));

    // Sample the end of the animation.
    await tester.pump(const Duration(milliseconds: 100));
    expect(tester.getTopLeft(find.text('item 0')).dy, lessThan(450));
    expect(tester.getTopLeft(find.text('item 0')).dy, greaterThan(400));

    // Wait for it to finish, it should be back to the original position
    await tester.pumpAndSettle();
    expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 400));
  });

  testWidgets('SliverReorderableList - properly animates the drop at starting position in a reversed list', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/84625
    final List<int> items = List<int>.generate(8, (int index) => index);

    Future<void> pressDragRelease(Offset start, Offset delta) async {
      final TestGesture drag = await tester.startGesture(start);
      await tester.pump(kPressTimeout);
      await drag.moveBy(delta);
      await tester.pumpAndSettle();
      await drag.up();
      await tester.pump();
    }

    // The TestList is 800x600 SliverReorderableList with 8 items 800x100 each.
    // Each item has a text widget with 'item $index' that can be moved by a
    // press and drag gesture. For this test we are reversing the order so
    // the first item is at the bottom.
    await tester.pumpWidget(TestList(items: items, reverse: true));

    expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 500));
    expect(tester.getTopLeft(find.text('item 1')), const Offset(0, 400));

    // Drag item 0 downwards off the edge and let it snap back. It should
    // smoothly animate back up.
    await pressDragRelease(tester.getCenter(find.text('item 0')), const Offset(0, 50));
    expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 550));
    expect(tester.getTopLeft(find.text('item 1')), const Offset(0, 400));

    // After the first several frames we should be moving closer to the final position,
    // not further away as was the case with the original bug.
    await tester.pump(const Duration(milliseconds: 10));
    expect(tester.getTopLeft(find.text('item 0')).dy, lessThan(550));

    // Sample the middle (don't use exact values as it depends on the internal
    // curve being used).
    await tester.pump(const Duration(milliseconds: 125));
    expect(tester.getTopLeft(find.text('item 0')).dy, lessThan(550));

    // Wait for it to finish, it should be back to the original position
    await tester.pumpAndSettle();
    expect(tester.getTopLeft(find.text('item 0')), const Offset(0, 500));
  });

  testWidgets('SliverReorderableList calls onReorderStart and onReorderEnd correctly', (WidgetTester tester) async {
    final List<int> items = List<int>.generate(8, (int index) => index);
    int? startIndex, endIndex;
    final Finder item0 = find.textContaining('item 0');

    await tester.pumpWidget(TestList(
      items: items,
      onReorderStart: (int index) {
        startIndex = index;
      },
      onReorderEnd: (int index) {
        endIndex = index;
      },
    ));

    TestGesture drag = await tester.startGesture(tester.getCenter(item0));
    await tester.pump(kPressTimeout);
    // Drag enough for move to start.
    await drag.moveBy(const Offset(0, 20));

    expect(startIndex, equals(0));
    expect(endIndex, isNull);

    // Move item0 from index 0 to index 3
    await drag.moveBy(const Offset(0, 300));
    await tester.pumpAndSettle();
    await drag.up();
    await tester.pumpAndSettle();

    expect(endIndex, equals(3));

    startIndex = null;
    endIndex = null;

    drag = await tester.startGesture(tester.getCenter(item0));
    await tester.pump(kPressTimeout);
    // Drag enough for move to start.
    await drag.moveBy(const Offset(0, 20));

    expect(startIndex, equals(2));
    expect(endIndex, isNull);

    // Move item0 from index 2 to index 0
    await drag.moveBy(const Offset(0, -200));
    await tester.pumpAndSettle();
    await drag.up();
    await tester.pumpAndSettle();

    expect(endIndex, equals(0));
  });

  testWidgets('ReorderableList calls onReorderStart and onReorderEnd correctly', (WidgetTester tester) async {
    final List<int> items = List<int>.generate(8, (int index) => index);
    int? startIndex, endIndex;
    final Finder item0 = find.textContaining('item 0');

    void handleReorder(int fromIndex, int toIndex) {
      if (toIndex > fromIndex) {
        toIndex -= 1;
      }
      items.insert(toIndex, items.removeAt(fromIndex));
    }

    await tester.pumpWidget(MaterialApp(
      home: ReorderableList(
        itemCount: items.length,
        itemBuilder: (BuildContext context, int index) {
          return SizedBox(
            key: ValueKey<int>(items[index]),
            height: 100,
            child: ReorderableDelayedDragStartListener(
              index: index,
              child: Text('item ${items[index]}'),
            ),
          );
        },
        onReorder: handleReorder,
        onReorderStart: (int index) {
          startIndex = index;
        },
        onReorderEnd: (int index) {
          endIndex = index;
        },
      ),
    ));

    TestGesture drag = await tester.startGesture(tester.getCenter(item0));
    await tester.pump(kLongPressTimeout);
    // Drag enough for move to start.
    await drag.moveBy(const Offset(0, 20));

    expect(startIndex, equals(0));
    expect(endIndex, isNull);

    // Move item0 from index 0 to index 3
    await drag.moveBy(const Offset(0, 300));
    await tester.pumpAndSettle();
    await drag.up();
    await tester.pumpAndSettle();

    expect(endIndex, equals(3));

    startIndex = null;
    endIndex = null;

    drag = await tester.startGesture(tester.getCenter(item0));
    await tester.pump(kLongPressTimeout);
    // Drag enough for move to start.
    await drag.moveBy(const Offset(0, 20));

    expect(startIndex, equals(2));
    expect(endIndex, isNull);

    // Move item0 from index 2 to index 0
    await drag.moveBy(const Offset(0, -200));
    await tester.pumpAndSettle();
    await drag.up();
    await tester.pumpAndSettle();

    expect(endIndex, equals(0));
  });



  testWidgets('ReorderableList asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async {
    final List<int> numbers = <int>[0,1,2];
    expect(() => ReorderableList(
      itemBuilder: (BuildContext context, int index) {
        return SizedBox(
            key: ValueKey<int>(numbers[index]),
            height: 20 + numbers[index] * 10,
            child: ReorderableDragStartListener(
              index: index,
              child: Text(numbers[index].toString()),
            )
        );
      },
      itemCount: numbers.length,
      itemExtent: 30,
      prototypeItem: const SizedBox(),
      onReorder: (int fromIndex, int toIndex) { },
    ), throwsAssertionError);
  });

  testWidgets('SliverReorderableList asserts on both non-null itemExtent and prototypeItem', (WidgetTester tester) async {
    final List<int> numbers = <int>[0,1,2];
    expect(() => SliverReorderableList(
      itemBuilder: (BuildContext context, int index) {
        return SizedBox(
            key: ValueKey<int>(numbers[index]),
            height: 20 + numbers[index] * 10,
            child: ReorderableDragStartListener(
              index: index,
              child: Text(numbers[index].toString()),
            )
        );
      },
      itemCount: numbers.length,
      itemExtent: 30,
      prototypeItem: const SizedBox(),
      onReorder: (int fromIndex, int toIndex) { },
    ), throwsAssertionError);
  });

  testWidgets('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async {
    final List<int> numbers = <int>[0,1,2];

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return ReorderableList(
                itemBuilder: (BuildContext context, int index) {
                  return SizedBox(
                    key: ValueKey<int>(numbers[index]),
                    // children with different heights
                    height: 20 + numbers[index] * 10,
                    child: ReorderableDragStartListener(
                      index: index,
                      child: Text(numbers[index].toString()),
                    )
                  );
                },
                itemCount: numbers.length,
                itemExtent: 30,
                onReorder: (int fromIndex, int toIndex) {
                  if (fromIndex < toIndex) {
                    toIndex--;
                  }
                  final int value = numbers.removeAt(fromIndex);
                  numbers.insert(toIndex, value);
                },
              );
            },
          ),
        ),
      )
    );

    final double item0Height = tester.getSize(find.text('0').hitTestable()).height;
    final double item1Height = tester.getSize(find.text('1').hitTestable()).height;
    final double item2Height = tester.getSize(find.text('2').hitTestable()).height;

    expect(item0Height, 30.0);
    expect(item1Height, 30.0);
    expect(item2Height, 30.0);
  });

  testWidgets('if prototypeItem is non-null, children have same extent in the scroll direction', (WidgetTester tester) async {
    final List<int> numbers = <int>[0,1,2];

    await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: StatefulBuilder(
              builder: (BuildContext context, StateSetter setState) {
                return ReorderableList(
                  itemBuilder: (BuildContext context, int index) {
                    return SizedBox(
                        key: ValueKey<int>(numbers[index]),
                        // children with different heights
                        height: 20 + numbers[index] * 10,
                        child: ReorderableDragStartListener(
                          index: index,
                          child: Text(numbers[index].toString()),
                        )
                    );
                  },
                  itemCount: numbers.length,
                  prototypeItem: const SizedBox(
                    height: 30,
                    child: Text('3'),
                  ),
                  onReorder: (int oldIndex, int newIndex) {  },
                );
              },
            ),
          ),
        )
    );

    final double item0Height = tester.getSize(find.text('0').hitTestable()).height;
    final double item1Height = tester.getSize(find.text('1').hitTestable()).height;
    final double item2Height = tester.getSize(find.text('2').hitTestable()).height;

    expect(item0Height, 30.0);
    expect(item1Height, 30.0);
    expect(item2Height, 30.0);
  });

  group('ReorderableDragStartListener', () {
    testWidgets('It should allow the item to be dragged when enabled is true', (WidgetTester tester) async {
      const int itemCount = 5;
      int onReorderCallCount = 0;
      final List<int> items = List<int>.generate(itemCount, (int index) => index);

      void handleReorder(int fromIndex, int toIndex) {
        onReorderCallCount += 1;
        if (toIndex > fromIndex) {
          toIndex -= 1;
        }
        items.insert(toIndex, items.removeAt(fromIndex));
      }
      // The list has five elements of height 100
      await tester.pumpWidget(
        MaterialApp(
          home: ReorderableList(
            itemCount: itemCount,
            itemBuilder: (BuildContext context, int index) {
              return SizedBox(
                key: ValueKey<int>(items[index]),
                height: 100,
                child: ReorderableDragStartListener(
                  index: index,
                  child: Text('item ${items[index]}'),
                ),
              );
            },
            onReorder: handleReorder,
          ),
        ),
      );

      // Start gesture on first item
      final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0')));
      await tester.pump(kPressTimeout);

      // Drag enough to move down the first item
      await drag.moveBy(const Offset(0, 50));
      await tester.pump();
      await drag.up();
      await tester.pumpAndSettle();

      expect(onReorderCallCount, 1);
      expect(items, orderedEquals(<int>[1, 0, 2, 3, 4]));
    });

    testWidgets('It should not allow the item to be dragged when enabled is false', (WidgetTester tester) async {
      const int itemCount = 5;
      int onReorderCallCount = 0;
      final List<int> items = List<int>.generate(itemCount, (int index) => index);

      void handleReorder(int fromIndex, int toIndex) {
        onReorderCallCount += 1;
        if (toIndex > fromIndex) {
          toIndex -= 1;
        }
        items.insert(toIndex, items.removeAt(fromIndex));
      }
      // The list has five elements of height 100
      await tester.pumpWidget(
        MaterialApp(
          home: ReorderableList(
            itemCount: itemCount,
            itemBuilder: (BuildContext context, int index) {
              return SizedBox(
                key: ValueKey<int>(items[index]),
                height: 100,
                child: ReorderableDragStartListener(
                  index: index,
                  enabled: false,
                  child: Text('item ${items[index]}'),
                ),
              );
            },
            onReorder: handleReorder,
          ),
        ),
      );

      // Start gesture on first item
      final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0')));
      await tester.pump(kLongPressTimeout);

      // Drag enough to move down the first item
      await drag.moveBy(const Offset(0, 50));
      await tester.pump();
      await drag.up();
      await tester.pumpAndSettle();

      expect(onReorderCallCount, 0);
      expect(items, orderedEquals(<int>[0, 1, 2, 3, 4]));
    });
  });

  group('ReorderableDelayedDragStartListener', () {
    testWidgets('It should allow the item to be dragged when enabled is true', (WidgetTester tester) async {
      const int itemCount = 5;
      int onReorderCallCount = 0;
      final List<int> items = List<int>.generate(itemCount, (int index) => index);

      void handleReorder(int fromIndex, int toIndex) {
        onReorderCallCount += 1;
        if (toIndex > fromIndex) {
          toIndex -= 1;
        }
        items.insert(toIndex, items.removeAt(fromIndex));
      }
      // The list has five elements of height 100
      await tester.pumpWidget(
        MaterialApp(
          home: ReorderableList(
            itemCount: itemCount,
            itemBuilder: (BuildContext context, int index) {
              return SizedBox(
                key: ValueKey<int>(items[index]),
                height: 100,
                child: ReorderableDelayedDragStartListener(
                  index: index,
                  child: Text('item ${items[index]}'),
                ),
              );
            },
            onReorder: handleReorder,
          ),
        ),
      );
      await tester.pumpAndSettle();

      // Start gesture on first item
      final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0')));
      await tester.pump(kLongPressTimeout);

      // Drag enough to move down the first item
      await drag.moveBy(const Offset(0, 50));
      await tester.pump();
      await drag.up();
      await tester.pumpAndSettle();

      expect(onReorderCallCount, 1);
      expect(items, orderedEquals(<int>[1, 0, 2, 3, 4]));
    });

    testWidgets('It should not allow the item to be dragged when enabled is false', (WidgetTester tester) async {
      const int itemCount = 5;
      int onReorderCallCount = 0;
      final List<int> items = List<int>.generate(itemCount, (int index) => index);

      void handleReorder(int fromIndex, int toIndex) {
        onReorderCallCount += 1;
        if (toIndex > fromIndex) {
          toIndex -= 1;
        }
        items.insert(toIndex, items.removeAt(fromIndex));
      }
      // The list has five elements of height 100
      await tester.pumpWidget(
        MaterialApp(
          home: ReorderableList(
            itemCount: itemCount,
            itemBuilder: (BuildContext context, int index) {
              return SizedBox(
                key: ValueKey<int>(items[index]),
                height: 100,
                child: ReorderableDelayedDragStartListener(
                  index: index,
                  enabled: false,
                  child: Text('item ${items[index]}'),
                ),
              );
            },
            onReorder: handleReorder,
          ),
        ),
      );

      // Start gesture on first item
      final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('item 0')));
      await tester.pump(kLongPressTimeout);

      // Drag enough to move down the first item
      await drag.moveBy(const Offset(0, 50));
      await tester.pump();
      await drag.up();
      await tester.pumpAndSettle();

      expect(onReorderCallCount, 0);
      expect(items, orderedEquals(<int>[0, 1, 2, 3, 4]));
    });
  });

  testWidgets('SliverReorderableList properly disposes items', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/105010
    const int itemCount = 5;
    final List<int> items = List<int>.generate(itemCount, (int index) => index);

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(),
          drawer: Drawer(
            child: Builder(
              builder: (BuildContext context) {
                return Column(
                  children: <Widget>[
                    Expanded(
                      child: CustomScrollView(
                        slivers: <Widget>[
                          SliverReorderableList(
                            itemCount: itemCount,
                            itemBuilder: (BuildContext context, int index) {
                              return Material(
                                key: ValueKey<String>('item-$index'),
                                child: ReorderableDragStartListener(
                                  index: index,
                                  child: ListTile(
                                    title: Text('item ${items[index]}'),
                                  ),
                                ),
                              );
                            },
                            onReorder: (int oldIndex, int newIndex) {},
                          ),
                        ],
                      ),
                    ),
                    TextButton(
                      onPressed: () {
                        Scaffold.of(context).closeDrawer();
                      },
                      child: const Text('Close drawer'),
                    ),
                  ],
                );
              }
            ),
          ),
        ),
      ),
    );

    await tester.tap(find.byIcon(Icons.menu));
    await tester.pumpAndSettle();

    final Finder item0 = find.text('item 0');
    expect(item0, findsOneWidget);

    // Start gesture on first item without drag up event.
    final TestGesture drag = await tester.startGesture(tester.getCenter(item0));
    await drag.moveBy(const Offset(0, 200));
    await tester.pump();

    await tester.tap(find.text('Close drawer'));
    await tester.pumpAndSettle();

    expect(item0, findsNothing);
  });

  testWidgets('SliverReorderableList auto scrolls speed is configurable', (WidgetTester tester) async {
    Future<void> pumpFor({
      required Duration duration,
      Duration interval = const Duration(milliseconds: 50),
    }) async {
      await tester.pump();

      int times = (duration.inMilliseconds / interval.inMilliseconds).ceil();
      while (times > 0) {
        await tester.pump(interval + const Duration(milliseconds: 1));
        await tester.idle();
        times--;
      }
    }

    Future<double> pumpListAndDrag({required double autoScrollerVelocityScalar}) async {
      final List<int> items = List<int>.generate(10, (int index) => index);
      final ScrollController scrollController = ScrollController();

      await tester.pumpWidget(
        MaterialApp(
          home: CustomScrollView(
            controller: scrollController,
            slivers: <Widget>[
              SliverReorderableList(
                itemBuilder: (BuildContext context, int index) {
                  return Container(
                    key: ValueKey<int>(items[index]),
                    height: 100,
                    color: items[index].isOdd ? Colors.red : Colors.green,
                    child: ReorderableDragStartListener(
                      index: index,
                      child: Row(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          Text('item ${items[index]}'),
                          const Icon(Icons.drag_handle),
                        ],
                      ),
                    ),
                  );
                },
                itemCount: items.length,
                onReorder: (int fromIndex, int toIndex) {},
                autoScrollerVelocityScalar: autoScrollerVelocityScalar,
              ),
            ],
          ),
        ),
      );

      expect(scrollController.offset, 0);

      final Finder item = find.text('item 0');
      final TestGesture drag = await tester.startGesture(tester.getCenter(item));

      // Drag just enough to touch the edge but not surpass it, so the
      // auto scroller is not yet triggered
      await drag.moveBy(const Offset(0, 500));
      await pumpFor(duration: const Duration(milliseconds: 200));

      expect(scrollController.offset, 0);

      // Now drag a little bit more so the auto scroller triggers
      await drag.moveBy(const Offset(0, 50));
      await pumpFor(
        duration: const Duration(milliseconds: 600),
        interval: Duration(milliseconds: (1000 / autoScrollerVelocityScalar).round()),
      );

      return scrollController.offset;
    }

    const double fastVelocityScalar = 20;
    final double offsetForFastScroller = await pumpListAndDrag(autoScrollerVelocityScalar: fastVelocityScalar);

    // Reset widget tree
    await tester.pumpWidget(const SizedBox());

    const double slowVelocityScalar = 5;
    final double offsetForSlowScroller = await pumpListAndDrag(autoScrollerVelocityScalar: slowVelocityScalar);

    expect(offsetForFastScroller / offsetForSlowScroller, fastVelocityScalar / slowVelocityScalar);
  });
}

class TestList extends StatelessWidget {
  const TestList({
    super.key,
    this.textColor,
    this.iconColor,
    this.proxyDecorator,
    required this.items,
    this.reverse = false,
    this.onReorderStart,
    this.onReorderEnd,
    this.autoScrollerVelocityScalar,
  });

  final List<int> items;
  final Color? textColor;
  final Color? iconColor;
  final ReorderItemProxyDecorator? proxyDecorator;
  final bool reverse;
  final void Function(int)? onReorderStart, onReorderEnd;
  final double? autoScrollerVelocityScalar;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: DefaultTextStyle(
          style: TextStyle(color: textColor),
          child: IconTheme(
            data: IconThemeData(color: iconColor),
            child: StatefulBuilder(
              builder: (BuildContext outerContext, StateSetter setState) {
                final List<int> items = this.items;
                return CustomScrollView(
                  reverse: reverse,
                  slivers: <Widget>[
                    SliverReorderableList(
                      itemBuilder: (BuildContext context, int index) {
                        return Container(
                          key: ValueKey<int>(items[index]),
                          height: 100,
                          color: items[index].isOdd ? Colors.red : Colors.green,
                          child: ReorderableDragStartListener(
                            index: index,
                            child: Row(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: <Widget>[
                                Text('item ${items[index]}'),
                                const Icon(Icons.drag_handle),
                              ],
                            ),
                          ),
                        );
                      },
                      itemCount: items.length,
                      onReorder: (int fromIndex, int toIndex) {
                        setState(() {
                          if (toIndex > fromIndex) {
                            toIndex -= 1;
                          }
                          items.insert(toIndex, items.removeAt(fromIndex));
                        });
                      },
                      proxyDecorator: proxyDecorator,
                      onReorderStart: onReorderStart,
                      onReorderEnd: onReorderEnd,
                      autoScrollerVelocityScalar: autoScrollerVelocityScalar,
                    ),
                  ],
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}