// 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:collection';
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import 'semantics_tester.dart';

final List<String> results = <String>[];

Set<TestRoute> routes = HashSet<TestRoute>();

class TestRoute extends Route<String?> with LocalHistoryRoute<String?> {
  TestRoute(this.name);
  final String name;

  @override
  List<OverlayEntry> get overlayEntries => _entries;

  final List<OverlayEntry> _entries = <OverlayEntry>[];

  void log(String s) {
    results.add('$name: $s');
  }

  @override
  void install() {
    log('install');
    final OverlayEntry entry = OverlayEntry(
      builder: (BuildContext context) => Container(),
      opaque: true,
    );
    _entries.add(entry);
    routes.add(this);
    super.install();
  }

  @override
  TickerFuture didPush() {
    log('didPush');
    return super.didPush();
  }

  @override
  void didAdd() {
    log('didAdd');
    super.didAdd();
  }

  @override
  void didReplace(Route<dynamic>? oldRoute) {
    expect(oldRoute, isA<TestRoute>());
    final TestRoute castRoute = oldRoute! as TestRoute;
    log('didReplace ${castRoute.name}');
    super.didReplace(castRoute);
  }

  @override
  bool didPop(String? result) {
    log('didPop $result');
    bool returnValue;
    if (returnValue = super.didPop(result)) {
      navigator!.finalizeRoute(this);
    }
    return returnValue;
  }

  @override
  void didPopNext(Route<dynamic> nextRoute) {
    expect(nextRoute, isA<TestRoute>());
    final TestRoute castRoute = nextRoute as TestRoute;
    log('didPopNext ${castRoute.name}');
    super.didPopNext(castRoute);
  }

  @override
  void didChangeNext(Route<dynamic>? nextRoute) {
    expect(nextRoute, anyOf(isNull, isA<TestRoute>()));
    final TestRoute? castRoute = nextRoute as TestRoute?;
    log('didChangeNext ${castRoute?.name}');
    super.didChangeNext(castRoute);
  }

  @override
  void dispose() {
    log('dispose');
    _entries.clear();
    routes.remove(this);
    super.dispose();
  }

}

Future<void> runNavigatorTest(
  WidgetTester tester,
  NavigatorState host,
  VoidCallback test,
  List<String> expectations, [
  List<String> expectationsAfterAnotherPump = const <String>[],
]) async {
  expect(host, isNotNull);
  test();
  expect(results, equals(expectations));
  results.clear();
  await tester.pump();
  expect(results, equals(expectationsAfterAnotherPump));
  results.clear();
}

void main() {
  testWidgets('Route settings', (WidgetTester tester) async {
    const RouteSettings settings = RouteSettings(name: 'A');
    expect(settings, hasOneLineDescription);
  });

  testWidgets('Route settings arguments', (WidgetTester tester) async {
    const RouteSettings settings = RouteSettings(name: 'A');
    expect(settings.arguments, isNull);

    final Object arguments = Object();
    final RouteSettings settings2 = RouteSettings(name: 'A', arguments: arguments);
    expect(settings2.arguments, same(arguments));
  });

  testWidgets('Route management - push, replace, pop sequence', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Navigator(
          key: navigatorKey,
          onGenerateRoute: (_) => TestRoute('initial'),
        ),
      ),
    );
    final NavigatorState host = navigatorKey.currentState!;
    await runNavigatorTest(
      tester,
      host,
      () { },
      <String>[
        'initial: install',
        'initial: didAdd',
        'initial: didChangeNext null',
      ],
    );
    late TestRoute second;
    await runNavigatorTest(
      tester,
      host,
      () { host.push(second = TestRoute('second')); },
      <String>[ // stack is: initial, second
        'second: install',
        'second: didPush',
        'second: didChangeNext null',
        'initial: didChangeNext second',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.push(TestRoute('third')); },
      <String>[ // stack is: initial, second, third
        'third: install',
        'third: didPush',
        'third: didChangeNext null',
        'second: didChangeNext third',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.replace(oldRoute: second, newRoute: TestRoute('two')); },
      <String>[ // stack is: initial, two, third
        'two: install',
        'two: didReplace second',
        'two: didChangeNext third',
        'initial: didChangeNext two',
        'second: dispose',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.pop('hello'); },
      <String>[ // stack is: initial, two
        'third: didPop hello',
        'two: didPopNext third',
      ],
      <String>[
        'third: dispose',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.pop('good bye'); },
      <String>[ // stack is: initial
        'two: didPop good bye',
        'initial: didPopNext two',
      ],
      <String>[
        'two: dispose',
      ],
    );
    await tester.pumpWidget(Container());
    expect(results, equals(<String>['initial: dispose']));
    expect(routes.isEmpty, isTrue);
    results.clear();
  });

  testWidgets('Route management - push, remove, pop', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Navigator(
          key: navigatorKey,
          onGenerateRoute: (_) => TestRoute('first'),
        ),
      ),
    );
    final NavigatorState host = navigatorKey.currentState!;
    await runNavigatorTest(
      tester,
      host,
      () { },
      <String>[
        'first: install',
        'first: didAdd',
        'first: didChangeNext null',
      ],
    );
    late TestRoute second;
    await runNavigatorTest(
      tester,
      host,
      () { host.push(second = TestRoute('second')); },
      <String>[
        'second: install',
        'second: didPush',
        'second: didChangeNext null',
        'first: didChangeNext second',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.push(TestRoute('third')); },
      <String>[
        'third: install',
        'third: didPush',
        'third: didChangeNext null',
        'second: didChangeNext third',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.removeRouteBelow(second); },
      <String>[
        'first: dispose',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.pop('good bye'); },
      <String>[
        'third: didPop good bye',
        'second: didPopNext third',
      ],
      <String>[
        'third: dispose',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.push(TestRoute('three')); },
      <String>[
        'three: install',
        'three: didPush',
        'three: didChangeNext null',
        'second: didChangeNext three',
      ],
    );
    late TestRoute four;
    await runNavigatorTest(
      tester,
      host,
      () { host.push(four = TestRoute('four')); },
      <String>[
        'four: install',
        'four: didPush',
        'four: didChangeNext null',
        'three: didChangeNext four',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.removeRouteBelow(four); },
      <String>[
        'second: didChangeNext four',
        'three: dispose',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.pop('the end'); },
      <String>[
        'four: didPop the end',
        'second: didPopNext four',
      ],
      <String>[
        'four: dispose',
      ],
    );
    await tester.pumpWidget(Container());
    expect(results, equals(<String>['second: dispose']));
    expect(routes.isEmpty, isTrue);
    results.clear();
  });

  testWidgets('Route management - push, replace, popUntil', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Navigator(
          key: navigatorKey,
          onGenerateRoute: (_) => TestRoute('A'),
        ),
      ),
    );
    final NavigatorState host = navigatorKey.currentState!;
    await runNavigatorTest(
      tester,
      host,
      () { },
      <String>[
        'A: install',
        'A: didAdd',
        'A: didChangeNext null',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.push(TestRoute('B')); },
      <String>[
        'B: install',
        'B: didPush',
        'B: didChangeNext null',
        'A: didChangeNext B',
      ],
    );
    late TestRoute routeC;
    await runNavigatorTest(
      tester,
      host,
      () { host.push(routeC = TestRoute('C')); },
      <String>[
        'C: install',
        'C: didPush',
        'C: didChangeNext null',
        'B: didChangeNext C',
      ],
    );
    expect(routeC.isActive, isTrue);
    late TestRoute routeB;
    await runNavigatorTest(
      tester,
      host,
      () { host.replaceRouteBelow(anchorRoute: routeC, newRoute: routeB = TestRoute('b')); },
      <String>[
        'b: install',
        'b: didReplace B',
        'b: didChangeNext C',
        'A: didChangeNext b',
        'B: dispose',
      ],
    );
    await runNavigatorTest(
      tester,
      host,
      () { host.popUntil((Route<dynamic> route) => route == routeB); },
      <String>[
        'C: didPop null',
        'b: didPopNext C',
      ],
      <String>[
        'C: dispose',
      ],
    );
    await tester.pumpWidget(Container());
    expect(results, equals(<String>['A: dispose', 'b: dispose']));
    expect(routes.isEmpty, isTrue);
    results.clear();
  });

  testWidgets('Route localHistory - popUntil', (WidgetTester tester) async {
    final TestRoute routeA = TestRoute('A');
    routeA.addLocalHistoryEntry(LocalHistoryEntry(
      onRemove: () { routeA.log('onRemove 0'); },
    ));
    routeA.addLocalHistoryEntry(LocalHistoryEntry(
      onRemove: () { routeA.log('onRemove 1'); },
    ));
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Navigator(
          key: navigatorKey,
          onGenerateRoute: (_) => routeA,
        ),
      ),
    );
    final NavigatorState host = navigatorKey.currentState!;
    await runNavigatorTest(
      tester,
      host,
      () { host.popUntil((Route<dynamic> route) => !route.willHandlePopInternally); },
      <String>[
        'A: install',
        'A: didAdd',
        'A: didChangeNext null',
        'A: didPop null',
        'A: onRemove 1',
        'A: didPop null',
        'A: onRemove 0',
      ],
    );

    await runNavigatorTest(
      tester,
      host,
      () { host.popUntil((Route<dynamic> route) => !route.willHandlePopInternally); },
      <String>[
      ],
    );
    await tester.pumpWidget(Container());
    expect(routes.isEmpty, isTrue);
    results.clear();
  });

  group('PageRouteObserver', () {
    test('calls correct listeners', () {
      final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
      final MockRouteAware pageRouteAware1 = MockRouteAware();
      final MockPageRoute route1 = MockPageRoute();
      observer.subscribe(pageRouteAware1, route1);
      expect(pageRouteAware1.didPushCount, 1);

      final MockRouteAware pageRouteAware2 = MockRouteAware();
      final MockPageRoute route2 = MockPageRoute();
      observer.didPush(route2, route1);
      expect(pageRouteAware1.didPushNextCount, 1);

      observer.subscribe(pageRouteAware2, route2);
      expect(pageRouteAware2.didPushCount, 1);

      observer.didPop(route2, route1);
      expect(pageRouteAware2.didPopCount, 1);
      expect(pageRouteAware1.didPopNextCount, 1);
    });

    test('does not call listeners for non-PageRoute', () {
      final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
      final MockRouteAware pageRouteAware = MockRouteAware();
      final MockPageRoute pageRoute = MockPageRoute();
      final MockRoute route = MockRoute();
      observer.subscribe(pageRouteAware, pageRoute);
      expect(pageRouteAware.didPushCount, 1);

      observer.didPush(route, pageRoute);
      observer.didPop(route, pageRoute);

      expect(pageRouteAware.didPushCount, 1);
      expect(pageRouteAware.didPopCount, 0);
    });

    test('does not call listeners when already subscribed', () {
      final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
      final MockRouteAware pageRouteAware = MockRouteAware();
      final MockPageRoute pageRoute = MockPageRoute();
      observer.subscribe(pageRouteAware, pageRoute);
      observer.subscribe(pageRouteAware, pageRoute);
      expect(pageRouteAware.didPushCount, 1);
    });

    test('does not call listeners when unsubscribed', () {
      final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
      final MockRouteAware pageRouteAware = MockRouteAware();
      final MockPageRoute pageRoute = MockPageRoute();
      final MockPageRoute nextPageRoute = MockPageRoute();
      observer.subscribe(pageRouteAware, pageRoute);
      observer.subscribe(pageRouteAware, nextPageRoute);
      expect(pageRouteAware.didPushCount, 2);

      observer.unsubscribe(pageRouteAware);

      observer.didPush(nextPageRoute, pageRoute);
      observer.didPop(nextPageRoute, pageRoute);

      expect(pageRouteAware.didPushCount, 2);
      expect(pageRouteAware.didPopCount, 0);
    });

    test('releases reference to route when unsubscribed', () {
      final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>();
      final MockRouteAware pageRouteAware = MockRouteAware();
      final MockRouteAware page2RouteAware = MockRouteAware();
      final MockPageRoute pageRoute = MockPageRoute();
      final MockPageRoute nextPageRoute = MockPageRoute();
      observer.subscribe(pageRouteAware, pageRoute);
      observer.subscribe(pageRouteAware, nextPageRoute);
      observer.subscribe(page2RouteAware, pageRoute);
      observer.subscribe(page2RouteAware, nextPageRoute);
      expect(pageRouteAware.didPushCount, 2);
      expect(page2RouteAware.didPushCount, 2);

      expect(observer.debugObservingRoute(pageRoute), true);
      expect(observer.debugObservingRoute(nextPageRoute), true);

      observer.unsubscribe(pageRouteAware);

      expect(observer.debugObservingRoute(pageRoute), true);
      expect(observer.debugObservingRoute(nextPageRoute), true);

      observer.unsubscribe(page2RouteAware);

      expect(observer.debugObservingRoute(pageRoute), false);
      expect(observer.debugObservingRoute(nextPageRoute), false);
    });
  });

  testWidgets('Can autofocus a TextField nested in a Focus in a route.', (WidgetTester tester) async {
    final TextEditingController controller = TextEditingController();

    final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
    await tester.pumpWidget(
      Material(
        child: MaterialApp(
          onGenerateRoute: (RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (BuildContext context, Animation<double> input, Animation<double> out) {
                return Focus(
                  child: TextField(
                    autofocus: true,
                    focusNode: focusNode,
                    controller: controller,
                  ),
                );
              },
            );
          },
        ),
      ),
    );
    await tester.pump();

    expect(focusNode.hasPrimaryFocus, isTrue);
  });

  group('PageRouteBuilder', () {
    testWidgets('reverseTransitionDuration defaults to 300ms', (WidgetTester tester) async {
      // Default PageRouteBuilder reverse transition duration should be 300ms.
      await tester.pumpWidget(
        MaterialApp(
          onGenerateRoute: (RouteSettings settings) {
            return MaterialPageRoute<dynamic>(
              builder: (BuildContext context) {
                return ElevatedButton(
                  onPressed: () {
                    Navigator.of(context).push<void>(
                      PageRouteBuilder<void>(
                        settings: settings,
                        pageBuilder: (BuildContext context, Animation<double> input, Animation<double> out) {
                          return const Text('Page Two');
                        },
                      ),
                    );
                  },
                  child: const Text('Open page'),
                );
              },
            );
          },
        ),
      );

      // Open the new route.
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();
      expect(find.text('Open page'), findsNothing);
      expect(find.text('Page Two'), findsOneWidget);

      // Pop the new route.
      tester.state<NavigatorState>(find.byType(Navigator)).pop();
      await tester.pump();
      expect(find.text('Page Two'), findsOneWidget);

      // Text('Page Two') should be present halfway through the reverse transition.
      await tester.pump(const Duration(milliseconds: 150));
      expect(find.text('Page Two'), findsOneWidget);

      // Text('Page Two') should be present at the very end of the reverse transition.
      await tester.pump(const Duration(milliseconds: 150));
      expect(find.text('Page Two'), findsOneWidget);

      // Text('Page Two') have transitioned out after 300ms.
      await tester.pump(const Duration(milliseconds: 1));
      expect(find.text('Page Two'), findsNothing);
      expect(find.text('Open page'), findsOneWidget);
    });

    testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push<void>(
                    PageRouteBuilder<void>(
                      settings: settings,
                      pageBuilder: (BuildContext context, Animation<double> input, Animation<double> out) {
                        return const Text('Page Two');
                      },
                      // modified value, default PageRouteBuilder reverse transition duration should be 300ms.
                      reverseTransitionDuration: const Duration(milliseconds: 150),
                    ),
                  );
                },
                child: const Text('Open page'),
              );
            },
          );
        },
      ));

      // Open the new route.
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();
      expect(find.text('Open page'), findsNothing);
      expect(find.text('Page Two'), findsOneWidget);

      // Pop the new route.
      tester.state<NavigatorState>(find.byType(Navigator)).pop();
      await tester.pump();
      expect(find.text('Page Two'), findsOneWidget);

      // Text('Page Two') should be present halfway through the reverse transition.
      await tester.pump(const Duration(milliseconds: 75));
      expect(find.text('Page Two'), findsOneWidget);

      // Text('Page Two') should be present at the very end of the reverse transition.
      await tester.pump(const Duration(milliseconds: 75));
      expect(find.text('Page Two'), findsOneWidget);

      // Text('Page Two') have transitioned out after 500ms.
      await tester.pump(const Duration(milliseconds: 1));
      expect(find.text('Page Two'), findsNothing);
      expect(find.text('Open page'), findsOneWidget);
    });
  });

  group('TransitionRoute', () {
    testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      await tester.pumpWidget(
        MaterialApp(
          navigatorKey: navigator,
          home: const Text('home'),
        ),
      );

      // Push page one, its secondary animation is kAlwaysDismissedAnimation.
      late ProxyAnimation secondaryAnimationProxyPageOne;
      late ProxyAnimation animationPageOne;
      navigator.currentState!.push(
        PageRouteBuilder<void>(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation;
            animationPageOne = animation as ProxyAnimation;
            return const Text('Page One');
          },
        ),
      );
      await tester.pump();
      await tester.pumpAndSettle();
      final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent! as ProxyAnimation;
      expect(animationPageOne.value, 1.0);
      expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);

      // Push page two, the secondary animation of page one is the primary
      // animation of page two.
      late ProxyAnimation secondaryAnimationProxyPageTwo;
      late ProxyAnimation animationPageTwo;
      navigator.currentState!.push(
        PageRouteBuilder<void>(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            secondaryAnimationProxyPageTwo = secondaryAnimation as ProxyAnimation;
            animationPageTwo = animation as ProxyAnimation;
            return const Text('Page Two');
          },
        ),
      );
      await tester.pump();
      await tester.pumpAndSettle();
      final ProxyAnimation secondaryAnimationPageTwo = secondaryAnimationProxyPageTwo.parent! as ProxyAnimation;
      expect(animationPageTwo.value, 1.0);
      expect(secondaryAnimationPageTwo.parent, kAlwaysDismissedAnimation);
      expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);

      // Pop page two, the secondary animation of page one becomes
      // kAlwaysDismissedAnimation.
      navigator.currentState!.pop();
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 100));
      expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);
      await tester.pumpAndSettle();
      expect(animationPageTwo.value, 0.0);
      expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
    });

    testWidgets('secondary animation is kDismissed when next route is removed', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      await tester.pumpWidget(
        MaterialApp(
          navigatorKey: navigator,
          home: const Text('home'),
        ),
      );

      // Push page one, its secondary animation is kAlwaysDismissedAnimation.
      late ProxyAnimation secondaryAnimationProxyPageOne;
      late ProxyAnimation animationPageOne;
      navigator.currentState!.push(
        PageRouteBuilder<void>(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation;
            animationPageOne = animation as ProxyAnimation;
            return const Text('Page One');
          },
        ),
      );
      await tester.pump();
      await tester.pumpAndSettle();
      final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent! as ProxyAnimation;
      expect(animationPageOne.value, 1.0);
      expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);

      // Push page two, the secondary animation of page one is the primary
      // animation of page two.
      late ProxyAnimation secondaryAnimationProxyPageTwo;
      late ProxyAnimation animationPageTwo;
      Route<void> secondRoute;
      navigator.currentState!.push(
        secondRoute = PageRouteBuilder<void>(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            secondaryAnimationProxyPageTwo = secondaryAnimation as ProxyAnimation;
            animationPageTwo = animation as ProxyAnimation;
            return const Text('Page Two');
          },
        ),
      );
      await tester.pump();
      await tester.pumpAndSettle();
      final ProxyAnimation secondaryAnimationPageTwo = secondaryAnimationProxyPageTwo.parent! as ProxyAnimation;
      expect(animationPageTwo.value, 1.0);
      expect(secondaryAnimationPageTwo.parent, kAlwaysDismissedAnimation);
      expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);

      // Remove the second route, the secondary animation of page one is
      // kAlwaysDismissedAnimation again.
      navigator.currentState!.removeRoute(secondRoute);
      await tester.pump();
      expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
    });

    testWidgets('secondary animation is kDismissed after train hopping finishes and pop', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      await tester.pumpWidget(
        MaterialApp(
          navigatorKey: navigator,
          home: const Text('home'),
        ),
      );

      // Push page one, its secondary animation is kAlwaysDismissedAnimation.
      late ProxyAnimation secondaryAnimationProxyPageOne;
      late ProxyAnimation animationPageOne;
      navigator.currentState!.push(
        PageRouteBuilder<void>(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation;
            animationPageOne = animation as ProxyAnimation;
            return const Text('Page One');
          },
        ),
      );
      await tester.pump();
      await tester.pumpAndSettle();
      final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent! as ProxyAnimation;
      expect(animationPageOne.value, 1.0);
      expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);

      // Push page two, the secondary animation of page one is the primary
      // animation of page two.
      late ProxyAnimation animationPageTwo;
      navigator.currentState!.push(
        PageRouteBuilder<void>(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            animationPageTwo = animation as ProxyAnimation;
            return const Text('Page Two');
          },
        ),
      );
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 100));
      expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);

      // Replace with a different route while push is ongoing to trigger
      // TrainHopping.
      late ProxyAnimation animationPageThree;
      navigator.currentState!.pushReplacement(
        TestPageRouteBuilder(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            animationPageThree = animation as ProxyAnimation;
            return const Text('Page Three');
          },
        ),
      );
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 1));
      expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
      final TrainHoppingAnimation trainHopper = secondaryAnimationPageOne.parent! as TrainHoppingAnimation;
      expect(trainHopper.currentTrain, animationPageTwo.parent);
      await tester.pump(const Duration(milliseconds: 100));
      expect(secondaryAnimationPageOne.parent, isNot(isA<TrainHoppingAnimation>()));
      expect(secondaryAnimationPageOne.parent, animationPageThree.parent);
      expect(trainHopper.currentTrain, isNull); // Has been disposed.
      await tester.pumpAndSettle();
      expect(secondaryAnimationPageOne.parent, animationPageThree.parent);

      // Pop page three.
      navigator.currentState!.pop();
      await tester.pump();
      await tester.pumpAndSettle();
      expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
    });

    testWidgets('secondary animation is kDismissed when train hopping is interrupted', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      await tester.pumpWidget(
        MaterialApp(
          navigatorKey: navigator,
          home: const Text('home'),
        ),
      );

      // Push page one, its secondary animation is kAlwaysDismissedAnimation.
      late ProxyAnimation secondaryAnimationProxyPageOne;
      late ProxyAnimation animationPageOne;
      navigator.currentState!.push(
        PageRouteBuilder<void>(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation;
            animationPageOne = animation as ProxyAnimation;
            return const Text('Page One');
          },
        ),
      );
      await tester.pump();
      await tester.pumpAndSettle();
      final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent! as ProxyAnimation;
      expect(animationPageOne.value, 1.0);
      expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);

      // Push page two, the secondary animation of page one is the primary
      // animation of page two.
      late ProxyAnimation animationPageTwo;
      navigator.currentState!.push(
        PageRouteBuilder<void>(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            animationPageTwo = animation as ProxyAnimation;
            return const Text('Page Two');
          },
        ),
      );
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 100));
      expect(secondaryAnimationPageOne.parent, animationPageTwo.parent);

      // Replace with a different route while push is ongoing to trigger
      // TrainHopping.
      navigator.currentState!.pushReplacement(
        TestPageRouteBuilder(
          pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
            return const Text('Page Three');
          },
        ),
      );
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 10));
      expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
      final TrainHoppingAnimation trainHopper = secondaryAnimationPageOne.parent! as TrainHoppingAnimation;
      expect(trainHopper.currentTrain, animationPageTwo.parent);

      // Pop page three while replacement push is ongoing.
      navigator.currentState!.pop();
      await tester.pump();
      expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>());
      final TrainHoppingAnimation trainHopper2 = secondaryAnimationPageOne.parent! as TrainHoppingAnimation;
      expect(trainHopper2.currentTrain, animationPageTwo.parent);
      expect(trainHopper.currentTrain, isNull); // Has been disposed.
      await tester.pumpAndSettle();
      expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation);
      expect(trainHopper2.currentTrain, isNull); // Has been disposed.
    });

    testWidgets('secondary animation is triggered when pop initial route', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      late Animation<double> secondaryAnimationOfRouteOne;
      late Animation<double> primaryAnimationOfRouteTwo;
      await tester.pumpWidget(
        MaterialApp(
          navigatorKey: navigator,
          onGenerateRoute: (RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                if (settings.name == '/') {
                  secondaryAnimationOfRouteOne = secondaryAnimation;
                } else {
                  primaryAnimationOfRouteTwo = animation;
                }
                return const Text('Page');
              },
            );
          },
          initialRoute: '/a',
        ),
      );
      // The secondary animation of the bottom route should be chained with the
      // primary animation of top most route.
      expect(secondaryAnimationOfRouteOne.value, 1.0);
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
      // Pops the top most route and verifies two routes are still chained.
      navigator.currentState!.pop();
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 30));
      expect(secondaryAnimationOfRouteOne.value, 0.9);
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
      await tester.pumpAndSettle();
      expect(secondaryAnimationOfRouteOne.value, 0.0);
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
    });

    testWidgets('showGeneralDialog handles transparent barrier color', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        home: Builder(
          builder: (BuildContext context) {
            return ElevatedButton(
              onPressed: () {
                showGeneralDialog<void>(
                  context: context,
                  barrierDismissible: true,
                  barrierLabel: 'barrier_label',
                  barrierColor: const Color(0x00000000),
                  transitionDuration: Duration.zero,
                  pageBuilder: (BuildContext innerContext, _, __) {
                    return const SizedBox();
                  },
                );
              },
              child: const Text('Show Dialog'),
            );
          },
        ),
      ));

      // Open the dialog.
      await tester.tap(find.byType(ElevatedButton));
      await tester.pump();
      expect(find.byType(ModalBarrier), findsNWidgets(2));

      // Close the dialog.
      await tester.tapAt(Offset.zero);
      await tester.pump();
      expect(find.byType(ModalBarrier), findsNWidgets(1));
    });

    testWidgets('showGeneralDialog adds non-dismissible barrier when barrierDismissible is false', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        home: Builder(
          builder: (BuildContext context) {
            return ElevatedButton(
              onPressed: () {
                showGeneralDialog<void>(
                  context: context,
                  transitionDuration: Duration.zero,
                  pageBuilder: (BuildContext innerContext, _, __) {
                    return const SizedBox();
                  },
                );
              },
              child: const Text('Show Dialog'),
            );
          },
        ),
      ));

      // Open the dialog.
      await tester.tap(find.byType(ElevatedButton));
      await tester.pump();
      expect(find.byType(ModalBarrier), findsNWidgets(2));
      final ModalBarrier barrier = find.byType(ModalBarrier).evaluate().last.widget as ModalBarrier;
      expect(barrier.dismissible, isFalse);

      // Close the dialog.
      final StatefulElement navigatorElement = find.byType(Navigator).evaluate().last as StatefulElement;
      final NavigatorState navigatorState = navigatorElement.state as NavigatorState;
      navigatorState.pop();
      await tester.pumpAndSettle();
      expect(find.byType(ModalBarrier), findsNWidgets(1));
    });

    testWidgets('showGeneralDialog uses null as a barrierLabel by default', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        home: Builder(
          builder: (BuildContext context) {
            return ElevatedButton(
              onPressed: () {
                showGeneralDialog<void>(
                  context: context,
                  transitionDuration: Duration.zero,
                  pageBuilder: (BuildContext innerContext, _, __) {
                    return const SizedBox();
                  },
                );
              },
              child: const Text('Show Dialog'),
            );
          },
        ),
      ));

      // Open the dialog.
      await tester.tap(find.byType(ElevatedButton));
      await tester.pump();
      expect(find.byType(ModalBarrier), findsNWidgets(2));
      final ModalBarrier barrier = find.byType(ModalBarrier).evaluate().last.widget as ModalBarrier;
      expect(barrier.semanticsLabel, same(null));

      // Close the dialog.
      final StatefulElement navigatorElement = find.byType(Navigator).evaluate().last as StatefulElement;
      final NavigatorState navigatorState = navigatorElement.state as NavigatorState;
      navigatorState.pop();
      await tester.pumpAndSettle();
      expect(find.byType(ModalBarrier), findsNWidgets(1));
    });

    testWidgets('showGeneralDialog uses root navigator by default', (WidgetTester tester) async {
      final DialogObserver rootObserver = DialogObserver();
      final DialogObserver nestedObserver = DialogObserver();

      await tester.pumpWidget(MaterialApp(
        navigatorObservers: <NavigatorObserver>[rootObserver],
        home: Navigator(
          observers: <NavigatorObserver>[nestedObserver],
          onGenerateRoute: (RouteSettings settings) {
            return MaterialPageRoute<dynamic>(
              builder: (BuildContext context) {
                return ElevatedButton(
                  onPressed: () {
                    showGeneralDialog<void>(
                      context: context,
                      transitionDuration: Duration.zero,
                      pageBuilder: (BuildContext innerContext, _, __) {
                        return const SizedBox();
                      },
                    );
                  },
                  child: const Text('Show Dialog'),
                );
              },
            );
          },
        ),
      ));

      // Open the dialog.
      await tester.tap(find.byType(ElevatedButton));

      expect(rootObserver.dialogCount, 1);
      expect(nestedObserver.dialogCount, 0);
    });

    testWidgets('showGeneralDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
      final DialogObserver rootObserver = DialogObserver();
      final DialogObserver nestedObserver = DialogObserver();

      await tester.pumpWidget(MaterialApp(
        navigatorObservers: <NavigatorObserver>[rootObserver],
        home: Navigator(
          observers: <NavigatorObserver>[nestedObserver],
          onGenerateRoute: (RouteSettings settings) {
            return MaterialPageRoute<dynamic>(
              builder: (BuildContext context) {
                return ElevatedButton(
                  onPressed: () {
                    showGeneralDialog<void>(
                      useRootNavigator: false,
                      context: context,
                      transitionDuration: Duration.zero,
                      pageBuilder: (BuildContext innerContext, _, __) {
                        return const SizedBox();
                      },
                    );
                  },
                  child: const Text('Show Dialog'),
                );
              },
            );
          },
        ),
      ));

      // Open the dialog.
      await tester.tap(find.byType(ElevatedButton));

      expect(rootObserver.dialogCount, 0);
      expect(nestedObserver.dialogCount, 1);
    });

    testWidgets('showGeneralDialog default argument values', (WidgetTester tester) async {
      final DialogObserver rootObserver = DialogObserver();

      await tester.pumpWidget(MaterialApp(
        navigatorObservers: <NavigatorObserver>[rootObserver],
        home: Navigator(
          onGenerateRoute: (RouteSettings settings) {
            return MaterialPageRoute<dynamic>(
              builder: (BuildContext context) {
                return ElevatedButton(
                  onPressed: () {
                    showGeneralDialog<void>(
                      context: context,
                      pageBuilder: (BuildContext innerContext, _, __) {
                        return const SizedBox();
                      },
                    );
                  },
                  child: const Text('Show Dialog'),
                );
              },
            );
          },
        ),
      ));

      // Open the dialog.
      await tester.tap(find.byType(ElevatedButton));
      expect(rootObserver.dialogRoutes.length, equals(1));
      final ModalRoute<dynamic> route = rootObserver.dialogRoutes.last;
      expect(route.barrierDismissible, isNotNull);
      expect(route.barrierColor, isNotNull);
      expect(route.transitionDuration, isNotNull);
    });

    group('showGeneralDialog avoids overlapping display features', () {
      testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
        await tester.pumpWidget(
          MaterialApp(
            builder: (BuildContext context, Widget? child) {
              return MediaQuery(
                // Display has a vertical hinge down the middle
                data: const MediaQueryData(
                  size: Size(800, 600),
                  displayFeatures: <DisplayFeature>[
                    DisplayFeature(
                      bounds: Rect.fromLTRB(390, 0, 410, 600),
                      type: DisplayFeatureType.hinge,
                      state: DisplayFeatureState.unknown,
                    ),
                  ],
                ),
                child: child!,
              );
            },
            home: const Center(child: Text('Test')),
          ),
        );
        final BuildContext context = tester.element(find.text('Test'));

        showGeneralDialog<void>(
          context: context,
          pageBuilder: (BuildContext context, _, __) {
            return const Placeholder();
          },
          anchorPoint: const Offset(1000, 0),
        );
        await tester.pumpAndSettle();

        // Should take the right side of the screen
        expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
        expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
      });

      testWidgets('positioning with Directionality', (WidgetTester tester) async {
        await tester.pumpWidget(
          MaterialApp(
            builder: (BuildContext context, Widget? child) {
              return MediaQuery(
                // Display has a vertical hinge down the middle
                data: const MediaQueryData(
                  size: Size(800, 600),
                  displayFeatures: <DisplayFeature>[
                    DisplayFeature(
                      bounds: Rect.fromLTRB(390, 0, 410, 600),
                      type: DisplayFeatureType.hinge,
                      state: DisplayFeatureState.unknown,
                    ),
                  ],
                ),
                child: Directionality(
                  textDirection: TextDirection.rtl,
                  child: child!,
                ),
              );
            },
            home: const Center(child: Text('Test')),
          ),
        );
        final BuildContext context = tester.element(find.text('Test'));

        showGeneralDialog<void>(
          context: context,
          pageBuilder: (BuildContext context, _, __) {
            return const Placeholder();
          },
        );
        await tester.pumpAndSettle();

        // Since this is RTL, it should place the dialog on the right screen
        expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
        expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
      });

      testWidgets('positioning by default', (WidgetTester tester) async {
        await tester.pumpWidget(
          MaterialApp(
            builder: (BuildContext context, Widget? child) {
              return MediaQuery(
                // Display has a vertical hinge down the middle
                data: const MediaQueryData(
                  size: Size(800, 600),
                  displayFeatures: <DisplayFeature>[
                    DisplayFeature(
                      bounds: Rect.fromLTRB(390, 0, 410, 600),
                      type: DisplayFeatureType.hinge,
                      state: DisplayFeatureState.unknown,
                    ),
                  ],
                ),
                child: child!,
              );
            },
            home: const Center(child: Text('Test')),
          ),
        );
        final BuildContext context = tester.element(find.text('Test'));

        showGeneralDialog<void>(
          context: context,
          pageBuilder: (BuildContext context, _, __) {
            return const Placeholder();
          },
        );
        await tester.pumpAndSettle();

        // By default it should place the dialog on the left screen
        expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero);
        expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(390.0, 600.0));
      });
    });

    testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async {
      final GlobalKey containerKey = GlobalKey();

      // Default MaterialPageRoute transition duration should be 300ms.
      await tester.pumpWidget(MaterialApp(
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push<void>(
                    MaterialPageRoute<void>(
                      builder: (BuildContext innerContext) {
                        return Container(
                          key: containerKey,
                          color: Colors.green,
                        );
                      },
                    ),
                  );
                },
                child: const Text('Open page'),
              );
            },
          );
        },
      ));

      // Open the new route.
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();
      expect(find.text('Open page'), findsNothing);
      expect(find.byKey(containerKey), findsOneWidget);

      // Pop the new route.
      tester.state<NavigatorState>(find.byType(Navigator)).pop();
      await tester.pump();
      expect(find.byKey(containerKey), findsOneWidget);

      // Container should be present halfway through the transition.
      await tester.pump(const Duration(milliseconds: 150));
      expect(find.byKey(containerKey), findsOneWidget);

      // Container should be present at the very end of the transition.
      await tester.pump(const Duration(milliseconds: 150));
      expect(find.byKey(containerKey), findsOneWidget);

      // Container have transitioned out after 300ms.
      await tester.pump(const Duration(milliseconds: 1));
      expect(find.byKey(containerKey), findsNothing);
    });

    testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async {
      final GlobalKey containerKey = GlobalKey();
      await tester.pumpWidget(MaterialApp(
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push<void>(
                    ModifiedReverseTransitionDurationRoute<void>(
                      builder: (BuildContext innerContext) {
                        return Container(
                          key: containerKey,
                          color: Colors.green,
                        );
                      },
                      // modified value, default MaterialPageRoute transition duration should be 300ms.
                      reverseTransitionDuration: const Duration(milliseconds: 150),
                    ),
                  );
                },
                child: const Text('Open page'),
              );
            },
          );
        },
      ));

      // Open the new route.
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();
      expect(find.text('Open page'), findsNothing);
      expect(find.byKey(containerKey), findsOneWidget);

      // Pop the new route.
      tester.state<NavigatorState>(find.byType(Navigator)).pop();
      await tester.pump();
      expect(find.byKey(containerKey), findsOneWidget);

      // Container should be present halfway through the transition.
      await tester.pump(const Duration(milliseconds: 75));
      expect(find.byKey(containerKey), findsOneWidget);

      // Container should be present at the very end of the transition.
      await tester.pump(const Duration(milliseconds: 75));
      expect(find.byKey(containerKey), findsOneWidget);

      // Container have transitioned out after 150ms.
      await tester.pump(const Duration(milliseconds: 1));
      expect(find.byKey(containerKey), findsNothing);
    });

    testWidgets('custom reverseTransitionDuration does not result in interrupted animations', (WidgetTester tester) async {
      final GlobalKey containerKey = GlobalKey();
      await tester.pumpWidget(MaterialApp(
        theme: ThemeData(
          pageTransitionsTheme: const PageTransitionsTheme(
            builders: <TargetPlatform, PageTransitionsBuilder>{
              TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), // use a fade transition
            },
          ),
        ),
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push<void>(
                    ModifiedReverseTransitionDurationRoute<void>(
                      builder: (BuildContext innerContext) {
                        return Container(
                          key: containerKey,
                          color: Colors.green,
                        );
                      },
                      // modified value, default MaterialPageRoute transition duration should be 300ms.
                      reverseTransitionDuration: const Duration(milliseconds: 150),
                    ),
                  );
                },
                child: const Text('Open page'),
              );
            },
          );
        },
      ));

      // Open the new route.
      await tester.tap(find.byType(ElevatedButton));
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 200)); // jump partway through the forward transition
      expect(find.byKey(containerKey), findsOneWidget);

      // Gets the opacity of the fade transition while animating forwards.
      final double topFadeTransitionOpacity = _getOpacity(containerKey, tester);

      // Pop the new route mid-transition.
      tester.state<NavigatorState>(find.byType(Navigator)).pop();
      await tester.pump();

      // Transition should not jump. In other words, the fade transition
      // opacity before and after animation changes directions should remain
      // the same.
      expect(_getOpacity(containerKey, tester), topFadeTransitionOpacity);

      // Reverse transition duration should be:
      // Forward transition elapsed time: 200ms / 300ms = 2 / 3
      // Reverse transition remaining time: 150ms * 2 / 3 = 100ms

      // Container should be present at the very end of the transition.
      await tester.pump(const Duration(milliseconds: 100));
      expect(find.byKey(containerKey), findsOneWidget);

      // Container have transitioned out after 100ms.
      await tester.pump(const Duration(milliseconds: 1));
      expect(find.byKey(containerKey), findsNothing);
    });
  });

  group('ModalRoute', () {
    testWidgets('default barrierCurve', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: Builder(
            builder: (BuildContext context) {
              return Center(
                child: ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    Navigator.of(context).push<void>(
                      _TestDialogRouteWithCustomBarrierCurve<void>(
                        child: const Text('Hello World'),
                      ),
                    );
                  },
                ),
              );
            },
          ),
        ),
      ));

      final CurveTween defaultBarrierTween = CurveTween(curve: Curves.ease);
      int getExpectedBarrierTweenAlphaValue(double t) {
        return Color.getAlphaFromOpacity(defaultBarrierTween.transform(t));
      }

      await tester.tap(find.text('X'));
      await tester.pump();
      final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier);
      expect(animatedModalBarrier, findsOneWidget);

      Animation<Color?> modalBarrierAnimation;
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(modalBarrierAnimation.value, Colors.transparent);

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.25), 1),
      );

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.50), 1),
      );

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.75), 1),
      );

      await tester.pumpAndSettle();
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(modalBarrierAnimation.value, Colors.black);
    });

    testWidgets('custom barrierCurve', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: Builder(
            builder: (BuildContext context) {
              return Center(
                child: ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    Navigator.of(context).push<void>(
                      _TestDialogRouteWithCustomBarrierCurve<void>(
                        child: const Text('Hello World'),
                        barrierCurve: Curves.linear,
                      ),
                    );
                  },
                ),
              );
            },
          ),
        ),
      ));

      final CurveTween customBarrierTween = CurveTween(curve: Curves.linear);
      int getExpectedBarrierTweenAlphaValue(double t) {
        return Color.getAlphaFromOpacity(customBarrierTween.transform(t));
      }

      await tester.tap(find.text('X'));
      await tester.pump();
      final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier);
      expect(animatedModalBarrier, findsOneWidget);

      Animation<Color?> modalBarrierAnimation;
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(modalBarrierAnimation.value, Colors.transparent);

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.25), 1),
      );

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.50), 1),
      );

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.75), 1),
      );

      await tester.pumpAndSettle();
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(modalBarrierAnimation.value, Colors.black);
    });

    testWidgets('white barrierColor', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: Builder(
            builder: (BuildContext context) {
              return Center(
                child: ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    Navigator.of(context).push<void>(
                      _TestDialogRouteWithCustomBarrierCurve<void>(
                        child: const Text('Hello World'),
                        barrierColor: Colors.white,
                      ),
                    );
                  },
                ),
              );
            },
          ),
        ),
      ));

      final CurveTween defaultBarrierTween = CurveTween(curve: Curves.ease);
      int getExpectedBarrierTweenAlphaValue(double t) {
        return Color.getAlphaFromOpacity(defaultBarrierTween.transform(t));
      }

      await tester.tap(find.text('X'));
      await tester.pump();
      final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier);
      expect(animatedModalBarrier, findsOneWidget);

      Animation<Color?> modalBarrierAnimation;
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(modalBarrierAnimation.value, Colors.white.withOpacity(0));

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.25), 1),
      );

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.50), 1),
      );

      await tester.pump(const Duration(milliseconds: 25));
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(
        modalBarrierAnimation.value!.alpha,
        closeTo(getExpectedBarrierTweenAlphaValue(0.75), 1),
      );

      await tester.pumpAndSettle();
      modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color;
      expect(modalBarrierAnimation.value, Colors.white);
    });

    testWidgets('modal route semantics order', (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/46625.
      final SemanticsTester semantics = SemanticsTester(tester);
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: Builder(
            builder: (BuildContext context) {
              return Center(
                child: ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    Navigator.of(context).push<void>(
                      _TestDialogRouteWithCustomBarrierCurve<void>(
                        child: const Text('Hello World'),
                        barrierLabel: 'test label',
                        barrierCurve: Curves.linear,
                      ),
                    );
                  },
                ),
              );
            },
          ),
        ),
      ));

      await tester.tap(find.text('X'));
      await tester.pumpAndSettle();
      expect(find.text('Hello World'), findsOneWidget);

      final TestSemantics expectedSemantics = TestSemantics.root(
        children: <TestSemantics>[
          TestSemantics.rootChild(
            id: 1,
            rect: TestSemantics.fullScreen,
            children: <TestSemantics>[
              TestSemantics(
                id: 6,
                rect: TestSemantics.fullScreen,
                children: <TestSemantics>[
                  TestSemantics(
                    id: 7,
                    rect: TestSemantics.fullScreen,
                    flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                    children: <TestSemantics>[
                      TestSemantics(
                        id: 8,
                        label: 'Hello World',
                        rect: TestSemantics.fullScreen,
                        textDirection: TextDirection.ltr,
                      ),
                    ],
                  ),
                ],
              ),
              // Modal barrier is put after modal scope
              TestSemantics(
                id: 5,
                rect: TestSemantics.fullScreen,
                actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
                label: 'test label',
                textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      )
      ;

      expect(semantics, hasSemantics(expectedSemantics));
      semantics.dispose();
    }, variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.iOS}));

    testWidgets('focus traverse correct when pop multiple page simultaneously', (WidgetTester tester) async {
      // Regression test: https://github.com/flutter/flutter/issues/48903
      final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
      await tester.pumpWidget(MaterialApp(
        navigatorKey: navigatorKey,
        home: const Text('dummy1'),
      ));
      final Element textOnPageOne = tester.element(find.text('dummy1'));
      final FocusScopeNode focusNodeOnPageOne = FocusScope.of(textOnPageOne);
      expect(focusNodeOnPageOne.hasFocus, isTrue);

      // Pushes one page.
      navigatorKey.currentState!.push<void>(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('dummy2'),
        ),
      );
      await tester.pumpAndSettle();

      final Element textOnPageTwo = tester.element(find.text('dummy2'));
      final FocusScopeNode focusNodeOnPageTwo = FocusScope.of(textOnPageTwo);
      // The focus should be on second page.
      expect(focusNodeOnPageOne.hasFocus, isFalse);
      expect(focusNodeOnPageTwo.hasFocus, isTrue);

      // Pushes another page.
      navigatorKey.currentState!.push<void>(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('dummy3'),
        ),
      );
      await tester.pumpAndSettle();
      final Element textOnPageThree = tester.element(find.text('dummy3'));
      final FocusScopeNode focusNodeOnPageThree = FocusScope.of(textOnPageThree);
      // The focus should be on third page.
      expect(focusNodeOnPageOne.hasFocus, isFalse);
      expect(focusNodeOnPageTwo.hasFocus, isFalse);
      expect(focusNodeOnPageThree.hasFocus, isTrue);

      // Pops two pages simultaneously.
      navigatorKey.currentState!.popUntil((Route<void> route) => route.isFirst);
      await tester.pumpAndSettle();
      // It should refocus page one after pops.
      expect(focusNodeOnPageOne.hasFocus, isTrue);
    });

    testWidgets('focus traversal is correct when popping multiple pages simultaneously - with focused children', (WidgetTester tester) async {
      // Regression test: https://github.com/flutter/flutter/issues/48903
      final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
      await tester.pumpWidget(MaterialApp(
        navigatorKey: navigatorKey,
        home: const Text('dummy1'),
      ));
      final Element textOnPageOne = tester.element(find.text('dummy1'));
      final FocusScopeNode focusNodeOnPageOne = FocusScope.of(textOnPageOne);
      expect(focusNodeOnPageOne.hasFocus, isTrue);

      // Pushes one page.
      navigatorKey.currentState!.push<void>(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Material(child: TextField()),
        ),
      );
      await tester.pumpAndSettle();

      final Element textOnPageTwo = tester.element(find.byType(TextField));
      final FocusScopeNode focusNodeOnPageTwo = FocusScope.of(textOnPageTwo);
      // The focus should be on second page.
      expect(focusNodeOnPageOne.hasFocus, isFalse);
      expect(focusNodeOnPageTwo.hasFocus, isTrue);

      // Move the focus to another node.
      focusNodeOnPageTwo.nextFocus();
      await tester.pumpAndSettle();
      expect(focusNodeOnPageTwo.hasFocus, isTrue);
      expect(focusNodeOnPageTwo.hasPrimaryFocus, isFalse);

      // Pushes another page.
      navigatorKey.currentState!.push<void>(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('dummy3'),
        ),
      );
      await tester.pumpAndSettle();
      final Element textOnPageThree = tester.element(find.text('dummy3'));
      final FocusScopeNode focusNodeOnPageThree = FocusScope.of(textOnPageThree);
      // The focus should be on third page.
      expect(focusNodeOnPageOne.hasFocus, isFalse);
      expect(focusNodeOnPageTwo.hasFocus, isFalse);
      expect(focusNodeOnPageThree.hasFocus, isTrue);

      // Pops two pages simultaneously.
      navigatorKey.currentState!.popUntil((Route<void> route) => route.isFirst);
      await tester.pumpAndSettle();
      // It should refocus page one after pops.
      expect(focusNodeOnPageOne.hasFocus, isTrue);
    });

    testWidgets('child with local history can be disposed', (WidgetTester tester) async {
      // Regression test: https://github.com/flutter/flutter/issues/52478
      await tester.pumpWidget(const MaterialApp(
        home: WidgetWithLocalHistory(),
      ));

      final WidgetWithLocalHistoryState state = tester.state(find.byType(WidgetWithLocalHistory));
      state.addLocalHistory();
      // Waits for modal route to update its internal state;
      await tester.pump();
      // Pumps a new widget to dispose WidgetWithLocalHistory. This should cause
      // it to remove the local history entry from modal route during
      // finalizeTree.
      await tester.pumpWidget(const MaterialApp(
        home: Text('dummy'),
      ));
      // Waits for modal route to update its internal state;
      await tester.pump();
      expect(tester.takeException(), null);
    });

    testWidgets('child with no local history can be disposed', (WidgetTester tester) async {
      await tester.pumpWidget(const MaterialApp(
        home: WidgetWithNoLocalHistory(),
      ));

      final WidgetWithNoLocalHistoryState state = tester.state(find.byType(WidgetWithNoLocalHistory));
      state.addLocalHistory();
      // Waits for modal route to update its internal state;
      await tester.pump();
      // Pumps a new widget to dispose WidgetWithNoLocalHistory. This should cause
      // it to remove the local history entry from modal route during
      // finalizeTree.
      await tester.pumpWidget(const MaterialApp(
        home: Text('dummy'),
      ));
      await tester.pump();
      expect(tester.takeException(), null);
    });
  });

  testWidgets('can be dismissed with escape keyboard shortcut', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    await tester.pumpWidget(MaterialApp(
      navigatorKey: navigatorKey,
      home: const Text('dummy1'),
    ));
    final Element textOnPageOne = tester.element(find.text('dummy1'));

    // Show a simple dialog
    showDialog<void>(
      context: textOnPageOne,
      builder: (BuildContext context) => const Text('dialog1'),
    );
    await tester.pumpAndSettle();
    expect(find.text('dialog1'), findsOneWidget);

    // Try to dismiss the dialog with the shortcut key
    await tester.sendKeyEvent(LogicalKeyboardKey.escape);
    await tester.pumpAndSettle();
    expect(find.text('dialog1'), findsNothing);
  });

  testWidgets('can not be dismissed with escape keyboard shortcut if barrier not dismissible', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    await tester.pumpWidget(MaterialApp(
      navigatorKey: navigatorKey,
      home: const Text('dummy1'),
    ));
    final Element textOnPageOne = tester.element(find.text('dummy1'));

    // Show a simple dialog
    showDialog<void>(
      context: textOnPageOne,
      barrierDismissible: false,
      builder: (BuildContext context) => const Text('dialog1'),
    );
    await tester.pumpAndSettle();
    expect(find.text('dialog1'), findsOneWidget);

    // Try to dismiss the dialog with the shortcut key
    await tester.sendKeyEvent(LogicalKeyboardKey.escape);
    await tester.pumpAndSettle();
    expect(find.text('dialog1'), findsOneWidget);
  });

  testWidgets('ModalRoute.of works for void routes', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    await tester.pumpWidget(MaterialApp(
      navigatorKey: navigatorKey,
      home: const Text('home'),
    ));
    expect(find.text('page2'), findsNothing);

    navigatorKey.currentState!.push<void>(MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return const Text('page2');
      },
    ));

    await tester.pumpAndSettle();
    expect(find.text('page2'), findsOneWidget);

    final ModalRoute<void>? parentRoute = ModalRoute.of<void>(tester.element(find.text('page2')));
    expect(parentRoute, isNotNull);
    expect(parentRoute, isA<MaterialPageRoute<void>>());
  });

  testWidgets('RawDialogRoute is state restorable', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        restorationScopeId: 'app',
        home: _RestorableDialogTestWidget(),
      ),
    );

    expect(find.byType(AlertDialog), findsNothing);

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    expect(find.byType(AlertDialog), findsOneWidget);
    final TestRestorationData restorationData = await tester.getRestorationData();

    await tester.restartAndRestore();

    expect(find.byType(AlertDialog), findsOneWidget);

    // Tap on the barrier.
    await tester.tapAt(const Offset(10.0, 10.0));
    await tester.pumpAndSettle();

    expect(find.byType(AlertDialog), findsNothing);

    await tester.restoreFrom(restorationData);
    expect(find.byType(AlertDialog), findsOneWidget);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
}

double _getOpacity(GlobalKey key, WidgetTester tester) {
  final Finder finder = find.ancestor(
    of: find.byKey(key),
    matching: find.byType(FadeTransition),
  );
  return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) {
    final FadeTransition transition = widget as FadeTransition;
    return a * transition.opacity.value;
  });
}

class ModifiedReverseTransitionDurationRoute<T> extends MaterialPageRoute<T> {
  ModifiedReverseTransitionDurationRoute({
    required super.builder,
    super.settings,
    required this.reverseTransitionDuration,
    super.fullscreenDialog,
  });

  @override
  final Duration reverseTransitionDuration;
}

class MockPageRoute extends Fake implements PageRoute<dynamic> { }

class MockRoute extends Fake implements Route<dynamic> { }

class MockRouteAware extends Fake implements RouteAware {
  int didPushCount = 0;
  int didPushNextCount = 0;
  int didPopCount = 0;
  int didPopNextCount = 0;

  @override
  void didPush() {
    didPushCount += 1;
  }

  @override
  void didPushNext() {
    didPushNextCount += 1;
  }

  @override
  void didPop() {
    didPopCount += 1;
  }

  @override
  void didPopNext() {
    didPopNextCount += 1;
  }
}

class TestPageRouteBuilder extends PageRouteBuilder<void> {
  TestPageRouteBuilder({required super.pageBuilder});

  @override
  Animation<double> createAnimation() {
    return CurvedAnimation(parent: super.createAnimation(), curve: Curves.easeOutExpo);
  }
}

class DialogObserver extends NavigatorObserver {
  final List<ModalRoute<dynamic>> dialogRoutes = <ModalRoute<dynamic>>[];
  int dialogCount = 0;

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route is RawDialogRoute) {
      dialogRoutes.add(route);
      dialogCount++;
    }
    super.didPush(route, previousRoute);
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route is RawDialogRoute) {
      dialogRoutes.removeLast();
      dialogCount--;
    }
    super.didPop(route, previousRoute);
  }
}

class _TestDialogRouteWithCustomBarrierCurve<T> extends PopupRoute<T> {
  _TestDialogRouteWithCustomBarrierCurve({
    required Widget child,
    this.barrierLabel,
    this.barrierColor = Colors.black,
    Curve? barrierCurve,
  }) : _barrierCurve = barrierCurve,
       _child = child;

  final Widget _child;

  @override
  bool get barrierDismissible => true;

  @override
  final String? barrierLabel;

  @override
  final Color? barrierColor;

  @override
  Curve get barrierCurve {
    if (_barrierCurve == null) {
      return super.barrierCurve;
    }
    return _barrierCurve!;
  }
  final Curve? _barrierCurve;

  @override
  Duration get transitionDuration => const Duration(milliseconds: 100); // easier value to test against

  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return Semantics(
      scopesRoute: true,
      explicitChildNodes: true,
      child: _child,
    );
  }
}

class WidgetWithLocalHistory extends StatefulWidget {
  const WidgetWithLocalHistory({super.key});

  @override
  WidgetWithLocalHistoryState createState() => WidgetWithLocalHistoryState();
}

class WidgetWithLocalHistoryState extends State<WidgetWithLocalHistory> {
  late LocalHistoryEntry _localHistory;

  void addLocalHistory() {
    final ModalRoute<dynamic> route = ModalRoute.of(context)!;
    _localHistory = LocalHistoryEntry();
    route.addLocalHistoryEntry(_localHistory);
  }

  @override
  void dispose() {
    super.dispose();
    _localHistory.remove();
  }

  @override
  Widget build(BuildContext context) {
    return const Text('dummy');
  }
}

class WidgetWithNoLocalHistory extends StatefulWidget {
  const WidgetWithNoLocalHistory({super.key});

  @override
  WidgetWithNoLocalHistoryState createState() => WidgetWithNoLocalHistoryState();
}

class WidgetWithNoLocalHistoryState extends State<WidgetWithNoLocalHistory> {
  late LocalHistoryEntry _localHistory;

  void addLocalHistory() {
    _localHistory = LocalHistoryEntry();
    // Not calling `route.addLocalHistoryEntry` here.
  }

  @override
  void dispose() {
    super.dispose();
    _localHistory.remove();
  }

  @override
  Widget build(BuildContext context) {
    return const Text('dummy');
  }
}

class _RestorableDialogTestWidget extends StatelessWidget {
  const _RestorableDialogTestWidget();

  @pragma('vm:entry-point')
  static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {
    return RawDialogRoute<void>(
      pageBuilder: (
        BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation,
      ) {
        return const AlertDialog(title: Text('Alert!'));
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: OutlinedButton(
          onPressed: () {
            Navigator.of(context).restorablePush(_dialogBuilder);
          },
          child: const Text('X'),
        ),
      ),
    );
  }
}
