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

void main() {
  final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
  const double kOpenScale = 1.15;

  Widget getChild() {
    return Container(
      width: 300.0,
      height: 100.0,
      color: CupertinoColors.activeOrange,
    );
  }

  Widget getBuilder(BuildContext context, Animation<double> animation) {
    return getChild();
  }

  Widget getContextMenu({
    Alignment alignment = Alignment.center,
    Size screenSize = const Size(800.0, 600.0),
    Widget? child,
  }) {
    return CupertinoApp(
      home: CupertinoPageScaffold(
        child: MediaQuery(
          data: MediaQueryData(size: screenSize),
          child: Align(
            alignment: alignment,
            child: CupertinoContextMenu(
              actions: <CupertinoContextMenuAction>[
                CupertinoContextMenuAction(
                  child: Text('CupertinoContextMenuAction $alignment'),
                ),
              ],
              child: child ?? getChild(),
            ),
          ),
        ),
      ),
    );
  }

  Widget getBuilderContextMenu({
    Alignment alignment = Alignment.center,
    Size screenSize = const Size(800.0, 600.0),
    CupertinoContextMenuBuilder? builder,
  }) {
    return CupertinoApp(
      home: CupertinoPageScaffold(
        child: MediaQuery(
          data: MediaQueryData(size: screenSize),
          child: Align(
            alignment: alignment,
            child: CupertinoContextMenu.builder(
              actions: <CupertinoContextMenuAction>[
                CupertinoContextMenuAction(
                  child: Text('CupertinoContextMenuAction $alignment'),
                ),
              ],
              builder: builder ?? getBuilder,
            ),
          ),
        ),
      ),
    );
  }

  // Finds the child widget that is rendered inside of _DecoyChild.
  Finder findDecoyChild(Widget child) {
    return find.descendant(
      of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'),
      matching: find.byWidget(child),
    );
  }

  // Finds the child widget rendered inside of _ContextMenuRouteStatic.
  Finder findStatic() {
    return find.descendant(
      of: find.byType(CupertinoApp),
      matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_ContextMenuRouteStatic'),
    );
  }

  Finder findStaticChild(Widget child) {
    return find.descendant(
      of: findStatic(),
      matching: find.byWidget(child),
    );
  }

  Finder findStaticChildDecoration(WidgetTester tester) {
    return find.descendant(
      of: findStatic(),
      matching: find.byType(DecoratedBox),
    );
  }

  Finder findFittedBox() {
    return find.descendant(
      of: findStatic(),
      matching: find.byType(FittedBox),
    );
  }

  Finder findStaticDefaultPreview() {
    return find.descendant(
      of: findFittedBox(),
      matching: find.byType(ClipRRect),
    );
  }

  group('CupertinoContextMenu before and during opening', () {
    testWidgetsWithLeakTracking('An unopened CupertinoContextMenu renders child in the same place as without', (WidgetTester tester) async {
      // Measure the child in the scene with no CupertinoContextMenu.
      final Widget child = getChild();
      await tester.pumpWidget(
        CupertinoApp(
          home: CupertinoPageScaffold(
            child: Center(
              child: child,
            ),
          ),
        ),
      );
      final Rect childRect = tester.getRect(find.byWidget(child));

      // When wrapped in a CupertinoContextMenu, the child is rendered in the same Rect.
      await tester.pumpWidget(getContextMenu(child: child));
      expect(find.byWidget(child), findsOneWidget);
      expect(tester.getRect(find.byWidget(child)), childRect);
    });

    testWidgetsWithLeakTracking('Can open CupertinoContextMenu by tap and hold', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(child: child));
      expect(find.byWidget(child), findsOneWidget);
      final Rect childRect = tester.getRect(find.byWidget(child));
      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsNothing);

      // Start a press on the child.
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pump();

      // The _DecoyChild is showing directly on top of the child.
      expect(findDecoyChild(child), findsOneWidget);
      Rect decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, equals(decoyChildRect));

      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsOneWidget);

      // After a small delay, the _DecoyChild has begun to animate.
      await tester.pump(const Duration(milliseconds: 400));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));

      // Eventually the decoy fully scales by _kOpenSize.
      await tester.pump(const Duration(milliseconds: 800));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));
      expect(decoyChildRect.width, childRect.width * kOpenScale);

      // Then the CupertinoContextMenu opens.
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);
    });

    testWidgetsWithLeakTracking('CupertinoContextMenu is in the correct position when within a nested navigator', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(CupertinoApp(
        home: CupertinoPageScaffold(
          child: MediaQuery(
            data: const MediaQueryData(size: Size(800, 600)),
            child: Align(
              alignment: Alignment.bottomRight,
              child: SizedBox(
                width: 700,
                height: 500,
                child: Navigator(
                  onGenerateRoute: (RouteSettings settings) {
                    return CupertinoPageRoute<void>(
                      builder: (BuildContext context) => Align(
                        child: CupertinoContextMenu(
                          actions: const <CupertinoContextMenuAction>[
                            CupertinoContextMenuAction(
                              child: Text('CupertinoContextMenuAction'),
                            ),
                          ],
                          child: child
                        ),
                      )
                    );
                  }
                )
              )
            )
          )
        )
      ));
      expect(find.byWidget(child), findsOneWidget);
      final Rect childRect = tester.getRect(find.byWidget(child));
      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsNothing);

      // Start a press on the child.
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pump();

      // The _DecoyChild is showing directly on top of the child.
      expect(findDecoyChild(child), findsOneWidget);
      Rect decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, equals(decoyChildRect));

      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsOneWidget);

      // After a small delay, the _DecoyChild has begun to animate.
      await tester.pump(const Duration(milliseconds: 400));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));

      // Eventually the decoy fully scales by _kOpenSize.
      await tester.pump(const Duration(milliseconds: 800));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));
      expect(decoyChildRect.width, childRect.width * kOpenScale);

      // Then the CupertinoContextMenu opens.
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);
    });

    testWidgetsWithLeakTracking('CupertinoContextMenu with a basic builder opens and closes the same as when providing a child', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(getBuilderContextMenu(builder: (BuildContext context, Animation<double> animation) {
        return child;
      }));
      expect(find.byWidget(child), findsOneWidget);
      final Rect childRect = tester.getRect(find.byWidget(child));
      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsNothing);

      // Start a press on the child.
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pump();

      // The _DecoyChild is showing directly on top of the child.
      expect(findDecoyChild(child), findsOneWidget);
      Rect decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, equals(decoyChildRect));

      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsOneWidget);

      // After a small delay, the _DecoyChild has begun to animate.
      await tester.pump(const Duration(milliseconds: 400));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));

      // Eventually the decoy fully scales by _kOpenSize.
      await tester.pump(const Duration(milliseconds: 800));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));
      expect(decoyChildRect.width, childRect.width * kOpenScale);

      // Then the CupertinoContextMenu opens.
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);
    });

    testWidgetsWithLeakTracking('CupertinoContextMenu with a builder can change the animation', (WidgetTester tester) async {
      await tester.pumpWidget(getBuilderContextMenu(builder: (BuildContext context, Animation<double> animation) {
        return Container(
          width: 300.0,
          height: 100.0,
          decoration: BoxDecoration(
            color: CupertinoColors.activeOrange,
            borderRadius: BorderRadius.circular(25.0 * animation.value)
          ),
        );
      }));

      final Widget child = find.descendant(of: find.byType(TickerMode), matching: find.byType(Container)).evaluate().single.widget;
      final Rect childRect = tester.getRect(find.byWidget(child));
      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsNothing);

      // Start a press on the child.
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pump();

      Finder findBuilderDecoyChild() {
        return find.descendant(
          of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'),
          matching: find.byType(Container),
        );
      }

      final Container decoyContainer = tester.firstElement(findBuilderDecoyChild()).widget as Container;
      final BoxDecoration? decoyDecoration = decoyContainer.decoration as BoxDecoration?;
      expect(decoyDecoration?.borderRadius, equals(BorderRadius.circular(0)));

      expect(findBuilderDecoyChild(), findsOneWidget);

      // After a small delay, the _DecoyChild has begun to animate with a different border radius.
      await tester.pump(const Duration(milliseconds: 500));
      final Container decoyLaterContainer = tester.firstElement(findBuilderDecoyChild()).widget as Container;
      final BoxDecoration? decoyLaterDecoration = decoyLaterContainer.decoration as BoxDecoration?;
      expect(decoyLaterDecoration?.borderRadius, isNot(equals(BorderRadius.circular(0))));

      // Finish gesture to release resources.
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
    });

    testWidgetsWithLeakTracking('Hovering over Cupertino context menu updates cursor to clickable on Web', (WidgetTester tester) async {
      final Widget child  = getChild();
      await tester.pumpWidget(CupertinoApp(
        home: CupertinoPageScaffold(
          child: Center(
            child: CupertinoContextMenu(
              actions: const <CupertinoContextMenuAction>[
                CupertinoContextMenuAction(
                  child: Text('CupertinoContextMenuAction One'),
                ),
              ],
              child: child,
            ),
          ),
        ),
      ));

      final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
      await gesture.addPointer(location: const Offset(10, 10));
      await tester.pumpAndSettle();
      expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);

      final Offset contextMenu = tester.getCenter(find.byWidget(child));
      await gesture.moveTo(contextMenu);
      await tester.pumpAndSettle();
      expect(
        RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
        kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
      );
    });

    testWidgetsWithLeakTracking('CupertinoContextMenu is in the correct position when within a Transform.scale', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(CupertinoApp(
        home: CupertinoPageScaffold(
          child: MediaQuery(
            data: const MediaQueryData(size: Size(800, 600)),
            child: Transform.scale(
              scale: 0.5,
              child: Align(
                //alignment: Alignment.bottomRight,
                child: CupertinoContextMenu(
                  actions: const <CupertinoContextMenuAction>[
                    CupertinoContextMenuAction(
                      child: Text('CupertinoContextMenuAction'),
                    ),
                  ],
                  child: child
                ),
              )
            )
          )
        )
      ));
      expect(find.byWidget(child), findsOneWidget);
      final Rect childRect = tester.getRect(find.byWidget(child));
      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsNothing);

      // Start a press on the child.
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pump();

      // The _DecoyChild is showing directly on top of the child.
      expect(findDecoyChild(child), findsOneWidget);
      Rect decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, equals(decoyChildRect));

      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsOneWidget);

      // After a small delay, the _DecoyChild has begun to animate.
      await tester.pump(const Duration(milliseconds: 400));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));

      // Eventually the decoy fully scales by _kOpenSize.
      await tester.pump(const Duration(milliseconds: 800));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));
      expect(decoyChildRect.width, childRect.width * kOpenScale);

      // Then the CupertinoContextMenu opens.
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);
    });
  });

  group('CupertinoContextMenu when open', () {
    testWidgetsWithLeakTracking('Last action does not have border', (WidgetTester tester) async {
      final Widget child  = getChild();
      await tester.pumpWidget(CupertinoApp(
        home: CupertinoPageScaffold(
          child: Center(
            child: CupertinoContextMenu(
              actions: const <CupertinoContextMenuAction>[
                CupertinoContextMenuAction(
                  child: Text('CupertinoContextMenuAction One'),
                ),
              ],
              child: child,
            ),
          ),
        ),
      ));

      // Open the CupertinoContextMenu
      final TestGesture firstGesture = await tester.startGesture(tester.getCenter(find.byWidget(child)));
      await tester.pumpAndSettle();
      await firstGesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);

      expect(findStaticChildDecoration(tester), findsNWidgets(1));

      // Close the CupertinoContextMenu.
      await tester.tapAt(const Offset(1.0, 1.0));
      await tester.pumpAndSettle();
      expect(findStatic(), findsNothing);

      await tester.pumpWidget(CupertinoApp(
        home: CupertinoPageScaffold(
          child: Center(
            child: CupertinoContextMenu(
              actions: const <CupertinoContextMenuAction>[
                CupertinoContextMenuAction(
                  child: Text('CupertinoContextMenuAction One'),
                ),
                CupertinoContextMenuAction(
                  child: Text('CupertinoContextMenuAction Two'),
                ),
              ],
              child: child,
            ),
          ),
        ),
      ));

      // Open the CupertinoContextMenu
      final TestGesture secondGesture = await tester.startGesture(tester.getCenter(find.byWidget(child)));
      await tester.pumpAndSettle();
      await secondGesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);

      expect(findStaticChildDecoration(tester), findsNWidgets(3));
    });

    testWidgetsWithLeakTracking('Can close CupertinoContextMenu by background tap', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(child: child));

      // Open the CupertinoContextMenu
      final Rect childRect = tester.getRect(find.byWidget(child));
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);

      // Tap and ensure that the CupertinoContextMenu is closed.
      await tester.tapAt(const Offset(1.0, 1.0));
      await tester.pumpAndSettle();
      expect(findStatic(), findsNothing);
    });

    testWidgetsWithLeakTracking('Can close CupertinoContextMenu by dragging down', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(child: child));

      // Open the CupertinoContextMenu
      final Rect childRect = tester.getRect(find.byWidget(child));
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);

      // Drag down not far enough and it bounces back and doesn't close.
      expect(findStaticChild(child), findsOneWidget);
      Offset staticChildCenter = tester.getCenter(findStaticChild(child));
      TestGesture swipeGesture = await tester.startGesture(staticChildCenter);
      await swipeGesture.moveBy(
        const Offset(0.0, 100.0),
        timeStamp: const Duration(milliseconds: 100),
      );
      await tester.pump();
      await swipeGesture.up();
      await tester.pump();
      expect(tester.getCenter(findStaticChild(child)).dy, greaterThan(staticChildCenter.dy));
      await tester.pumpAndSettle();
      expect(tester.getCenter(findStaticChild(child)), equals(staticChildCenter));
      expect(findStatic(), findsOneWidget);

      // Drag down far enough and it does close.
      expect(findStaticChild(child), findsOneWidget);
      staticChildCenter = tester.getCenter(findStaticChild(child));
      swipeGesture = await tester.startGesture(staticChildCenter);
      await swipeGesture.moveBy(
        const Offset(0.0, 200.0),
        timeStamp: const Duration(milliseconds: 100),
      );
      await tester.pump();
      await swipeGesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsNothing);
    });

    testWidgetsWithLeakTracking('Can close CupertinoContextMenu by flinging down', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(child: child));

      // Open the CupertinoContextMenu
      final Rect childRect = tester.getRect(find.byWidget(child));
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);

      // Fling up and nothing happens.
      expect(findStaticChild(child), findsOneWidget);
      await tester.fling(findStaticChild(child), const Offset(0.0, -100.0), 1000.0);
      await tester.pumpAndSettle();
      expect(findStaticChild(child), findsOneWidget);

      // Fling down to close the menu.
      expect(findStaticChild(child), findsOneWidget);
      await tester.fling(findStaticChild(child), const Offset(0.0, 100.0), 1000.0);
      await tester.pumpAndSettle();
      expect(findStatic(), findsNothing);
    });

    testWidgetsWithLeakTracking("Backdrop is added using ModalRoute's filter parameter", (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(child: child));
      expect(find.byType(BackdropFilter), findsNothing);

      // Open the CupertinoContextMenu
      final Rect childRect = tester.getRect(find.byWidget(child));
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);
      expect(find.byType(BackdropFilter), findsOneWidget);
    });

    testWidgetsWithLeakTracking('Preview widget should have the correct border radius', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(child: child));

      // Open the CupertinoContextMenu.
      final Rect childRect = tester.getRect(find.byWidget(child));
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);

      // Check border radius.
      expect(findStaticDefaultPreview(), findsOneWidget);
      final ClipRRect previewWidget = tester.firstWidget(findStaticDefaultPreview()) as ClipRRect;
      expect(previewWidget.borderRadius, equals(BorderRadius.circular(12.0)));
    });

    testWidgetsWithLeakTracking('CupertinoContextMenu width is correct', (WidgetTester tester) async {
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(child: child));
      expect(find.byWidget(child), findsOneWidget);
      final Rect childRect = tester.getRect(find.byWidget(child));
      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsNothing);

      // Start a press on the child.
      final TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pump();

      // The _DecoyChild is showing directly on top of the child.
      expect(findDecoyChild(child), findsOneWidget);
      Rect decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, equals(decoyChildRect));

      expect(find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DecoyChild'), findsOneWidget);

      // After a small delay, the _DecoyChild has begun to animate.
      await tester.pump(const Duration(milliseconds: 400));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));

      // Eventually the decoy fully scales by _kOpenSize.
      await tester.pump(const Duration(milliseconds: 800));
      decoyChildRect = tester.getRect(findDecoyChild(child));
      expect(childRect, isNot(equals(decoyChildRect)));
      expect(decoyChildRect.width, childRect.width * kOpenScale);

      // Then the CupertinoContextMenu opens.
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();
      expect(findStatic(), findsOneWidget);

      // The CupertinoContextMenu has the correct width and height.
      final CupertinoContextMenu widget = tester.widget(find.byType(CupertinoContextMenu));
      for (final Widget action in widget.actions) {
        // The value of the height is 80 because of the font and icon size.
        expect(tester.getSize(find.byWidget(action)).width, 250);
      }
    });

    testWidgetsWithLeakTracking("ContextMenu route animation doesn't throw exception on dismiss", (WidgetTester tester) async {
      // This is a regression test for https://github.com/flutter/flutter/issues/124597.
      final List<int> items = List<int>.generate(2, (int index) => index).toList();

      await tester.pumpWidget(CupertinoApp(
        home: CupertinoPageScaffold(
          child: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              return ListView(
                children: items.map((int index) => CupertinoContextMenu(
                  actions: <CupertinoContextMenuAction>[
                    CupertinoContextMenuAction(
                      child: const Text('DELETE'),
                      onPressed: () {
                        setState(() {
                          items.remove(index);
                          Navigator.of(context).pop();
                        });
                        Navigator.of(context).pop();
                      },
                    ),
                  ],
                  child: Text('Item $index'),
                )).toList(),
              );
            }
          ),
        ),
      ));

      // Open the CupertinoContextMenu.
      final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Item 1')));
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();

      // Tap the delete action.
      await tester.tap(find.text('DELETE'));
      await tester.pumpAndSettle();

      // The CupertinoContextMenu should be closed with no exception.
      expect(find.text('DELETE'), findsNothing);
      expect(tester.takeException(), null);
    });
  });

  group("Open layout differs depending on child's position on screen", () {
    testWidgetsWithLeakTracking('Portrait', (WidgetTester tester) async {
      const Size portraitScreenSize = Size(600.0, 800.0);
      await binding.setSurfaceSize(portraitScreenSize);

      // Pump a CupertinoContextMenu in the center of the screen and open it.
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(
        screenSize: portraitScreenSize,
        child: child,
      ));
      expect(find.byType(CupertinoContextMenuAction), findsNothing);
      Rect childRect = tester.getRect(find.byWidget(child));
      TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();

      // The position of the action is in the center of the screen.
      expect(find.byType(CupertinoContextMenuAction), findsOneWidget);
      final Offset center = tester.getTopLeft(find.byType(CupertinoContextMenuAction));

      // Close the CupertinoContextMenu.
      await tester.tapAt(const Offset(1.0, 1.0));
      await tester.pumpAndSettle();
      expect(findStatic(), findsNothing);

      // Pump a CupertinoContextMenu on the left of the screen and open it.
      await tester.pumpWidget(getContextMenu(
        alignment: Alignment.centerLeft,
        screenSize: portraitScreenSize,
        child: child,
      ));
      expect(find.byType(CupertinoContextMenuAction), findsNothing);
      await tester.pumpAndSettle();
      childRect = tester.getRect(find.byWidget(child));
      gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();

      // The position of the action is on the left of the screen.
      expect(find.byType(CupertinoContextMenuAction), findsOneWidget);
      final Offset left = tester.getTopLeft(find.byType(CupertinoContextMenuAction));
      expect(left.dx, lessThan(center.dx));

      // Close the CupertinoContextMenu.
      await tester.tapAt(const Offset(1.0, 1.0));
      await tester.pumpAndSettle();
      expect(findStatic(), findsNothing);

      // Pump a CupertinoContextMenu on the right of the screen and open it.
      await tester.pumpWidget(getContextMenu(
        alignment: Alignment.centerRight,
        screenSize: portraitScreenSize,
        child: child,
      ));
      expect(find.byType(CupertinoContextMenuAction), findsNothing);
      childRect = tester.getRect(find.byWidget(child));
      gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();

      // The position of the action is on the right of the screen.
      expect(find.byType(CupertinoContextMenuAction), findsOneWidget);
      final Offset right = tester.getTopLeft(find.byType(CupertinoContextMenuAction));
      expect(right.dx, greaterThan(center.dx));

      // Set the screen back to its normal size.
      await binding.setSurfaceSize(const Size(800.0, 600.0));
    });

    testWidgetsWithLeakTracking('Landscape', (WidgetTester tester) async {
      // Pump a CupertinoContextMenu in the center of the screen and open it.
      final Widget child = getChild();
      await tester.pumpWidget(getContextMenu(
        child: child,
      ));
      expect(find.byType(CupertinoContextMenuAction), findsNothing);
      Rect childRect = tester.getRect(find.byWidget(child));
      TestGesture gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();

      // Landscape doesn't support a centered action list, so the action is on
      // the left side of the screen.
      expect(find.byType(CupertinoContextMenuAction), findsOneWidget);
      final Offset center = tester.getTopLeft(find.byType(CupertinoContextMenuAction));

      // Close the CupertinoContextMenu.
      await tester.tapAt(const Offset(1.0, 1.0));
      await tester.pumpAndSettle();
      expect(findStatic(), findsNothing);

      // Pump a CupertinoContextMenu on the left of the screen and open it.
      await tester.pumpWidget(getContextMenu(
        alignment: Alignment.centerLeft,
        child: child,
      ));
      expect(find.byType(CupertinoContextMenuAction), findsNothing);
      childRect = tester.getRect(find.byWidget(child));
      gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();

      // The position of the action is on the right of the screen, which is the
      // same as for center aligned children in landscape.
      expect(find.byType(CupertinoContextMenuAction), findsOneWidget);
      final Offset left = tester.getTopLeft(find.byType(CupertinoContextMenuAction));
      expect(left.dx, equals(center.dx));

      // Close the CupertinoContextMenu.
      await tester.tapAt(const Offset(1.0, 1.0));
      await tester.pumpAndSettle();
      expect(findStatic(), findsNothing);

      // Pump a CupertinoContextMenu on the right of the screen and open it.
      await tester.pumpWidget(getContextMenu(
        alignment: Alignment.centerRight,
        child: child,
      ));
      expect(find.byType(CupertinoContextMenuAction), findsNothing);
      childRect = tester.getRect(find.byWidget(child));
      gesture = await tester.startGesture(childRect.center);
      await tester.pumpAndSettle();
      await gesture.up();
      await tester.pumpAndSettle();

      // The position of the action is on the left of the screen.
      expect(find.byType(CupertinoContextMenuAction), findsOneWidget);
      final Offset right = tester.getTopLeft(find.byType(CupertinoContextMenuAction));
      expect(right.dx, lessThan(left.dx));
    });
  });

  testWidgetsWithLeakTracking('Conflicting gesture detectors', (WidgetTester tester) async {
    int? onPointerDownTime;
    int? onPointerUpTime;
    bool insideTapTriggered = false;
    // The required duration of the route to be pushed in is [500, 900]ms.
    // 500ms is calculated from kPressTimeout+_previewLongPressTimeout/2.
    // 900ms is calculated from kPressTimeout+_previewLongPressTimeout.
    const Duration pressDuration = Duration(milliseconds: 501);

    int now() => clock.now().millisecondsSinceEpoch;

    await tester.pumpWidget(Listener(
      onPointerDown: (PointerDownEvent event) => onPointerDownTime = now(),
      onPointerUp: (PointerUpEvent event) => onPointerUpTime = now(),
      child: CupertinoApp(
        home: Align(
          child: CupertinoContextMenu(
            actions: const <CupertinoContextMenuAction>[
              CupertinoContextMenuAction(
                child: Text('CupertinoContextMenuAction'),
              ),
            ],
            child: GestureDetector(
              onTap: () => insideTapTriggered = true,
              child: Container(
                width: 200,
                height: 200,
                key: const Key('container'),
                color: const Color(0xFF00FF00),
              ),
            ),
          ),
        ),
      ),
    ));

    // Start a press on the child.
    final TestGesture gesture = await tester.createGesture();
    await gesture.down(tester.getCenter(find.byKey(const Key('container'))));
    // Simulate the actual situation:
    // the user keeps pressing and requesting frames.
    // If there is only one frame,
    // the animation is mutant and cannot drive the value of the animation controller.
    for (int i = 0; i < 100; i++) {
      await tester.pump(pressDuration ~/ 100);
    }
    await gesture.up();
    // Await pushing route.
    await tester.pumpAndSettle();

    // Judge whether _ContextMenuRouteStatic present on the screen.
    final Finder routeStatic = find.byWidgetPredicate(
          (Widget w) => '${w.runtimeType}' == '_ContextMenuRouteStatic',
    );

    // The insideTap and the route should not be triggered at the same time.
    if (insideTapTriggered) {
      // Calculate the actual duration.
      final int actualDuration = onPointerUpTime! - onPointerDownTime!;

      expect(routeStatic, findsNothing,
          reason: 'When actualDuration($actualDuration) is in the range of 500ms~900ms, '
              'which means the route is pushed, '
              'but insideTap should not be triggered at the same time.');
    } else {
      // The route should be pushed when the insideTap is not triggered.
      expect(routeStatic, findsOneWidget);
    }
  });
}
