// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';

void main() {
  testWidgetsWithLeakTracking('Restoration Smoke Test', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());

    expect(findRoute('home', count: 0), findsOneWidget);
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    await tester.restartAndRestore();
    expect(findRoute('home', count: 1), findsOneWidget);

    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 2), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 3), findsOneWidget);

    await tester.restoreFrom(data);
    expect(findRoute('home', count: 2), findsOneWidget);
  });

  testWidgetsWithLeakTracking('restorablePushNamed', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo', arguments: 3);
    await tester.pumpAndSettle();

    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('Foo', count: 0, arguments: 3), findsOneWidget);

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2, arguments: 3), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar', arguments: 4);
    await tester.pumpAndSettle();
    expect(findRoute('Bar', arguments: 4), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
    expect(findRoute('Bar'), findsNothing);
  });

  testWidgetsWithLeakTracking('restorablePushReplacementNamed', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacementNamed('Foo', arguments: 3);
    await tester.pumpAndSettle();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0, arguments: 3), findsOneWidget);

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2, arguments: 3), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar', arguments: 4);
    await tester.pumpAndSettle();
    expect(findRoute('Bar', arguments: 4), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
    expect(findRoute('Bar'), findsNothing);
  });

  testWidgetsWithLeakTracking('restorablePopAndPushNamed', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePopAndPushNamed('Foo', arguments: 3);
    await tester.pumpAndSettle();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0, arguments: 3), findsOneWidget);

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2, arguments: 3), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar', arguments: 4);
    await tester.pumpAndSettle();
    expect(findRoute('Bar', arguments: 4), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
    expect(findRoute('Bar'), findsNothing);
  });

  testWidgetsWithLeakTracking('restorablePushNamedAndRemoveUntil', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamedAndRemoveUntil('Foo', (Route<dynamic> _) => false, arguments: 3);
    await tester.pumpAndSettle();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0, arguments: 3), findsOneWidget);

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2, arguments: 3), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar', arguments: 4);
    await tester.pumpAndSettle();
    expect(findRoute('Bar', arguments: 4), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
    expect(findRoute('Bar'), findsNothing);
  });

  testWidgetsWithLeakTracking('restorablePush', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeBuilder, arguments: 'Foo');
    await tester.pumpAndSettle();

    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('Foo', count: 0), findsOneWidget);

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 1), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('Foo', count: 1), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
    await tester.pumpAndSettle();
    expect(findRoute('Bar'), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('Foo', count: 1), findsOneWidget);
    expect(findRoute('Bar'), findsNothing);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615

  testWidgetsWithLeakTracking('restorablePush adds route on all platforms', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeBuilder, arguments: 'Foo');
    await tester.pumpAndSettle();
    expect(findRoute('Foo'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('restorablePushReplacement', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home', count: 0), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacement(_routeBuilder, arguments: 'Foo');
    await tester.pumpAndSettle();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0), findsOneWidget);

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 1), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
    await tester.pumpAndSettle();
    expect(findRoute('Bar'), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1), findsOneWidget);
    expect(findRoute('Bar'), findsNothing);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615

  testWidgetsWithLeakTracking('restorablePushReplacement adds route on all platforms', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacement(_routeBuilder, arguments: 'Foo');
    await tester.pumpAndSettle();
    expect(findRoute('Foo'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('restorablePushAndRemoveUntil', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home', count: 0), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushAndRemoveUntil(_routeBuilder, (Route<dynamic> _) => false, arguments: 'Foo');
    await tester.pumpAndSettle();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0), findsOneWidget);

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 1), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
    await tester.pumpAndSettle();
    expect(findRoute('Bar'), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1), findsOneWidget);
    expect(findRoute('Bar'), findsNothing);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615

  testWidgetsWithLeakTracking('restorablePushAndRemoveUntil adds route on all platforms', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushAndRemoveUntil(_routeBuilder, (Route<dynamic> _) => false, arguments: 'Foo');
    await tester.pumpAndSettle();
    expect(findRoute('Foo'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('restorableReplace', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home', count: 0), findsOneWidget);

    final Route<Object> oldRoute = ModalRoute.of(tester.element(find.text('Route: home')))!;
    expect(oldRoute.settings.name, 'home');

    tester.state<NavigatorState>(find.byType(Navigator)).restorableReplace(newRouteBuilder: _routeBuilder, arguments: 'Foo', oldRoute: oldRoute);
    await tester.pumpAndSettle();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0), findsOneWidget);

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 1), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
    await tester.pumpAndSettle();
    expect(findRoute('Bar'), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 1), findsOneWidget);
    expect(findRoute('Bar'), findsNothing);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615

  testWidgetsWithLeakTracking('restorableReplace adds route on all platforms', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home', count: 0), findsOneWidget);

    final Route<Object> oldRoute = ModalRoute.of(tester.element(find.text('Route: home')))!;
    expect(oldRoute.settings.name, 'home');

    tester.state<NavigatorState>(find.byType(Navigator)).restorableReplace(newRouteBuilder: _routeBuilder, arguments: 'Foo', oldRoute: oldRoute);
    await tester.pumpAndSettle();
    expect(findRoute('Foo'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('restorableReplaceRouteBelow', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home', count: 0), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Anchor');
    await tester.pumpAndSettle();

    await tapRouteCounter('Anchor', tester);
    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 0, skipOffstage: false), findsOneWidget);
    expect(findRoute('Anchor', count: 1), findsOneWidget);

    final Route<Object> anchor = ModalRoute.of(tester.element(find.text('Route: Anchor')))!;
    expect(anchor.settings.name, 'Anchor');

    tester.state<NavigatorState>(find.byType(Navigator)).restorableReplaceRouteBelow(newRouteBuilder: _routeBuilder, arguments: 'Foo', anchorRoute: anchor);
    await tester.pumpAndSettle();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0, skipOffstage: false), findsOneWidget);
    expect(findRoute('Anchor', count: 1), findsOneWidget);

    await tapRouteCounter('Anchor', tester);
    expect(findRoute('Anchor', count: 2), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0, skipOffstage: false), findsOneWidget);
    expect(findRoute('Anchor', count: 2), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Anchor', tester);
    expect(findRoute('Anchor', count: 3), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
    await tester.pumpAndSettle();
    expect(findRoute('Bar'), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('home', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 0, skipOffstage: false), findsOneWidget);
    expect(findRoute('Anchor', count: 2), findsOneWidget);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615

  testWidgetsWithLeakTracking('restorableReplaceRouteBelow adds route on all platforms', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home', count: 0), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Anchor');
    await tester.pumpAndSettle();

    await tapRouteCounter('Anchor', tester);
    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 0, skipOffstage: false), findsOneWidget);
    expect(findRoute('Anchor', count: 1), findsOneWidget);

    final Route<Object> anchor = ModalRoute.of(tester.element(find.text('Route: Anchor')))!;
    expect(anchor.settings.name, 'Anchor');

    tester.state<NavigatorState>(find.byType(Navigator)).restorableReplaceRouteBelow(newRouteBuilder: _routeBuilder, arguments: 'Foo', anchorRoute: anchor);
    await tester.pumpAndSettle();
    expect(findRoute('Foo', skipOffstage: false), findsOneWidget);
  });

  testWidgetsWithLeakTracking('restoring a popped route', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
    await tester.pumpAndSettle();

    await tapRouteCounter('Foo', tester);
    await tapRouteCounter('Foo', tester);
    expect(findRoute('home'), findsNothing);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('Foo', count: 2), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();

    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 3), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('Foo'), findsNothing);

    await tester.restoreFrom(data);

    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('Foo', count: 2), findsOneWidget);
  });

  testWidgetsWithLeakTracking('popped routes are not restored', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
    await tester.pumpAndSettle();
    expect(findRoute('Foo'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
    await tester.pumpAndSettle();
    expect(findRoute('Bar'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();

    expect(findRoute('Bar'), findsNothing);
    expect(findRoute('Foo'), findsOneWidget);
    expect(findRoute('home', skipOffstage: false), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('Bar'), findsNothing);
    expect(findRoute('Foo'), findsOneWidget);
    expect(findRoute('home', skipOffstage: false), findsOneWidget);
  });

  testWidgetsWithLeakTracking('routes that are in the process of push are restored', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
    await tester.pump();
    await tester.pump();
    expect(findRoute('Foo'), findsOneWidget);

    // Push is in progress.
    final ModalRoute<Object> route1 = ModalRoute.of(tester.element(find.text('Route: Foo')))!;
    final String route1id = route1.restorationScopeId.value!;
    expect(route1id, isNotNull);
    expect(route1.settings.name, 'Foo');
    expect(route1.animation!.isCompleted, isFalse);
    expect(route1.animation!.isDismissed, isFalse);
    expect(route1.isActive, isTrue);

    await tester.restartAndRestore();

    expect(findRoute('Foo'), findsOneWidget);
    expect(findRoute('home', skipOffstage: false), findsOneWidget);
    final ModalRoute<Object> route2 = ModalRoute.of(tester.element(find.text('Route: Foo')))!;
    expect(route2, isNot(same(route1)));
    expect(route1.restorationScopeId.value, route1id);
    expect(route2.animation!.isCompleted, isTrue);
    expect(route2.isActive, isTrue);
  });

  testWidgetsWithLeakTracking('routes that are in the process of pop are not restored', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
    await tester.pumpAndSettle();

    final ModalRoute<Object> route1 = ModalRoute.of(tester.element(find.text('Route: Foo')))!;
    int notifyCount = 0;
    route1.restorationScopeId.addListener(() {
      notifyCount++;
    });
    expect(route1.isActive, isTrue);
    expect(route1.restorationScopeId.value, isNotNull);
    expect(route1.animation!.isCompleted, isTrue);
    expect(notifyCount, 0);

    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    expect(notifyCount, 1);
    await tester.pump();
    await tester.pump();

    // Pop is in progress.
    expect(route1.restorationScopeId.value, isNull);
    expect(route1.settings.name, 'Foo');
    expect(route1.animation!.isCompleted, isFalse);
    expect(route1.animation!.isDismissed, isFalse);
    expect(route1.isActive, isFalse);

    await tester.restartAndRestore();

    expect(findRoute('Foo', skipOffstage: false), findsNothing);
    expect(findRoute('home', count: 1), findsOneWidget);
    expect(notifyCount, 1);
  });

  testWidgetsWithLeakTracking('routes are restored in the right order', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1');
    await tester.pumpAndSettle();
    expect(findRoute('route1'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route2');
    await tester.pumpAndSettle();
    expect(findRoute('route2'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route3');
    await tester.pumpAndSettle();
    expect(findRoute('route3'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route4');
    await tester.pumpAndSettle();
    expect(findRoute('route4'), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('route4'), findsOneWidget);
    expect(findRoute('route3', skipOffstage: false), findsOneWidget);
    expect(findRoute('route2', skipOffstage: false), findsOneWidget);
    expect(findRoute('route1', skipOffstage: false), findsOneWidget);
    expect(findRoute('home', skipOffstage: false), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('route3'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('route2'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('route1'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('home'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('all routes up to first unrestorable are restored', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1');
    await tester.pumpAndSettle();
    expect(findRoute('route1'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route2');
    await tester.pumpAndSettle();
    expect(findRoute('route2'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('route3');
    await tester.pumpAndSettle();
    expect(findRoute('route3'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route4');
    await tester.pumpAndSettle();
    expect(findRoute('route4'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeBuilder, arguments: 'route5');
    await tester.pumpAndSettle();
    expect(findRoute('route5'), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('route5', skipOffstage: false), findsNothing);
    expect(findRoute('route4', skipOffstage: false), findsNothing);
    expect(findRoute('route3', skipOffstage: false), findsNothing);

    expect(findRoute('route2'), findsOneWidget);
    expect(findRoute('route1', skipOffstage: false), findsOneWidget);
    expect(findRoute('home', skipOffstage: false), findsOneWidget);
  });

  testWidgetsWithLeakTracking('removing unrestorable routes restores all of them', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1');
    await tester.pumpAndSettle();
    expect(findRoute('route1'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route2');
    await tester.pumpAndSettle();
    expect(findRoute('route2'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('route3');
    await tester.pumpAndSettle();
    expect(findRoute('route3'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route4');
    await tester.pumpAndSettle();
    expect(findRoute('route4'), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route5');
    await tester.pumpAndSettle();
    expect(findRoute('route5'), findsOneWidget);

    final Route<Object> route = ModalRoute.of(tester.element(find.text('Route: route3', skipOffstage: false)))!;
    expect(route.settings.name, 'route3');
    tester.state<NavigatorState>(find.byType(Navigator)).removeRoute(route);
    await tester.pumpAndSettle();

    await tester.restartAndRestore();

    expect(findRoute('route5'), findsOneWidget);
    expect(findRoute('route4', skipOffstage: false), findsOneWidget);
    expect(findRoute('route3', skipOffstage: false), findsNothing);
    expect(findRoute('route2', skipOffstage: false), findsOneWidget);
    expect(findRoute('route1', skipOffstage: false), findsOneWidget);
    expect(findRoute('home', skipOffstage: false), findsOneWidget);
  });

  testWidgetsWithLeakTracking('RestorableRouteFuture', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeFutureBuilder);
    await tester.pumpAndSettle();
    expect(find.text('Return value: null'), findsOneWidget);

    final RestorableRouteFuture<int> routeFuture = tester
        .state<RouteFutureWidgetState>(find.byType(RouteFutureWidget))
        .routeFuture;
    expect(routeFuture.route, isNull);
    expect(routeFuture.isPresent, isFalse);
    expect(routeFuture.enabled, isFalse);

    routeFuture.present('Foo');
    await tester.pumpAndSettle();
    expect(find.text('Route: Foo'), findsOneWidget);
    expect(routeFuture.route!.settings.name, 'Foo');
    expect(routeFuture.isPresent, isTrue);
    expect(routeFuture.enabled, isTrue);

    await tester.restartAndRestore();

    expect(find.text('Route: Foo'), findsOneWidget);
    final RestorableRouteFuture<int> restoredRouteFuture = tester
        .state<RouteFutureWidgetState>(find.byType(RouteFutureWidget, skipOffstage: false))
        .routeFuture;
    expect(restoredRouteFuture.route!.settings.name, 'Foo');
    expect(restoredRouteFuture.isPresent, isTrue);
    expect(restoredRouteFuture.enabled, isTrue);

    tester.state<NavigatorState>(find.byType(Navigator)).pop(10);
    await tester.pumpAndSettle();
    expect(find.text('Return value: 10'), findsOneWidget);
    expect(restoredRouteFuture.route, isNull);
    expect(restoredRouteFuture.isPresent, isFalse);
    expect(restoredRouteFuture.enabled, isFalse);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615

  testWidgetsWithLeakTracking('RestorableRouteFuture in unrestorable context', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    expect(findRoute('home'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('unrestorable');
    await tester.pumpAndSettle();
    expect(findRoute('unrestorable'), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeFutureBuilder);
    await tester.pumpAndSettle();
    expect(find.text('Return value: null'), findsOneWidget);

    final RestorableRouteFuture<int> routeFuture = tester
        .state<RouteFutureWidgetState>(find.byType(RouteFutureWidget))
        .routeFuture;
    expect(routeFuture.route, isNull);
    expect(routeFuture.isPresent, isFalse);
    expect(routeFuture.enabled, isFalse);

    routeFuture.present('Foo');
    await tester.pumpAndSettle();
    expect(find.text('Route: Foo'), findsOneWidget);
    expect(routeFuture.route!.settings.name, 'Foo');
    expect(routeFuture.isPresent, isTrue);
    expect(routeFuture.enabled, isFalse);

    await tester.restartAndRestore();

    expect(findRoute('home'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('Illegal arguments throw', (WidgetTester tester) async {
    await tester.pumpWidget(const TestWidget());
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
    await tester.pumpAndSettle();

    final Route<Object> oldRoute = ModalRoute.of(tester.element(find.text('Route: Bar')))!;
    expect(oldRoute.settings.name, 'Bar');

    final Matcher throwsArgumentsAssertionError = throwsA(isAssertionError.having(
      (AssertionError e) => e.message,
      'message',
      'The arguments object must be serializable via the StandardMessageCodec.',
    ));
    final Matcher throwsBuilderAssertionError = throwsA(isAssertionError.having(
      (AssertionError e) => e.message,
      'message',
      'The provided routeBuilder must be a static function.',
    ));

    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo', arguments: Object()),
      throwsArgumentsAssertionError,
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacementNamed('Foo', arguments: Object()),
      throwsArgumentsAssertionError,
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePopAndPushNamed('Foo', arguments: Object()),
      throwsArgumentsAssertionError,
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamedAndRemoveUntil('Foo', (Route<Object?> _) => false, arguments: Object()),
      throwsArgumentsAssertionError,
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeBuilder, arguments: Object()),
      throwsArgumentsAssertionError,
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacement(_routeBuilder, arguments: Object()),
      throwsArgumentsAssertionError,
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushAndRemoveUntil(_routeBuilder, (Route<Object?> _) => false, arguments: Object()),
      throwsArgumentsAssertionError,
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorableReplace(newRouteBuilder: _routeBuilder, oldRoute: oldRoute, arguments: Object()),
      throwsArgumentsAssertionError,
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorableReplaceRouteBelow(newRouteBuilder: _routeBuilder, anchorRoute: oldRoute, arguments: Object()),
      throwsArgumentsAssertionError,
    );

    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePush((BuildContext _, Object? __) => FakeRoute()),
      throwsBuilderAssertionError,
      skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacement((BuildContext _, Object? __) => FakeRoute()),
      throwsBuilderAssertionError,
      skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushAndRemoveUntil((BuildContext _, Object? __) => FakeRoute(), (Route<Object?> _) => false),
      throwsBuilderAssertionError,
      skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorableReplace(newRouteBuilder: (BuildContext _, Object? __) => FakeRoute(), oldRoute: oldRoute),
      throwsBuilderAssertionError,
      skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
    );
    expect(
      () => tester.state<NavigatorState>(find.byType(Navigator)).restorableReplaceRouteBelow(newRouteBuilder: (BuildContext _, Object? __) => FakeRoute(), anchorRoute: oldRoute),
      throwsBuilderAssertionError,
      skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
    );
  });

  testWidgetsWithLeakTracking('Moving scopes', (WidgetTester tester) async {
    await tester.pumpWidget(const RootRestorationScope(
      restorationId: 'root',
      child: TestWidget(
        restorationId: null,
      ),
    ));
    await tapRouteCounter('home', tester);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
    await tester.pumpAndSettle();
    expect(findRoute('Foo'), findsOneWidget);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);

    // Nothing is restored.
    await tester.restartAndRestore();
    expect(findRoute('Foo'), findsNothing);
    expect(findRoute('home', count: 0), findsOneWidget);

    await tapRouteCounter('home', tester);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
    await tester.pumpAndSettle();

    // Move navigator into restoration scope.
    await tester.pumpWidget(const RootRestorationScope(
      restorationId: 'root',
      child: TestWidget(),
    ));

    expect(findRoute('Foo'), findsOneWidget);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);

    // Everything is restored.
    await tester.restartAndRestore();
    expect(findRoute('Foo'), findsOneWidget);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);

    // Move navigator out of restoration scope.
    await tester.pumpWidget(const RootRestorationScope(
      restorationId: 'root',
      child: TestWidget(
        restorationId: null,
      ),
    ));

    expect(findRoute('Foo'), findsOneWidget);
    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);

    // Nothing is restored.
    await tester.restartAndRestore();
    expect(findRoute('Foo'), findsNothing);
    expect(findRoute('home', count: 0), findsOneWidget);
  });

  testWidgetsWithLeakTracking('Restoring pages', (WidgetTester tester) async {
    await tester.pumpWidget(const PagedTestWidget());
    expect(findRoute('home', count: 0), findsOneWidget);
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
    await tester.pumpAndSettle();
    await tapRouteCounter('Foo', tester);
    await tapRouteCounter('Foo', tester);
    expect(findRoute('Foo', count: 2), findsOneWidget);

    final TestRestorationData data = await tester.getRestorationData();
    await tester.restartAndRestore();

    expect(findRoute('Foo', count: 2), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('bar');
    await tester.pumpAndSettle();
    await tapRouteCounter('bar', tester);
    expect(findRoute('bar', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
    await tester.pumpAndSettle();
    expect(findRoute('Foo', count: 0), findsOneWidget);

    await tester.restoreFrom(data);

    expect(findRoute('bar', skipOffstage: false), findsNothing);
    expect(findRoute('Foo', count: 2), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('bar');
    await tester.pumpAndSettle();
    expect(findRoute('bar', count: 0), findsOneWidget);
  });

  testWidgetsWithLeakTracking('Unrestorable pages', (WidgetTester tester) async {
    await tester.pumpWidget(const PagedTestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);
    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p1');
    await tester.pumpAndSettle();
    await tapRouteCounter('p1', tester);
    expect(findRoute('p1', count: 1), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('r1');
    await tester.pumpAndSettle();
    await tapRouteCounter('r1', tester);
    expect(findRoute('r1', count: 1), findsOneWidget);

    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p2', restoreState: false);
    await tester.pumpAndSettle();
    await tapRouteCounter('p2', tester);
    expect(findRoute('p2', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('r2');
    await tester.pumpAndSettle();
    await tapRouteCounter('r2', tester);
    expect(findRoute('r2', count: 1), findsOneWidget);

    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p3');
    await tester.pumpAndSettle();
    await tapRouteCounter('p3', tester);
    expect(findRoute('p3', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('r3');
    await tester.pumpAndSettle();
    await tapRouteCounter('r3', tester);
    expect(findRoute('r3', count: 1), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('r2', skipOffstage: false), findsNothing);
    expect(findRoute('r3', count: 1), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('p3', count: 1), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('p2', count: 0), findsOneWidget); // Page did not restore its state!
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('r1', count: 1), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('p1', count: 1), findsOneWidget);
    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pumpAndSettle();
    expect(findRoute('home', count: 1), findsOneWidget);
  });

  testWidgetsWithLeakTracking('removed page is not restored', (WidgetTester tester) async {
    await tester.pumpWidget(const PagedTestWidget());
    await tapRouteCounter('home', tester);
    expect(findRoute('home', count: 1), findsOneWidget);

    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p1');
    await tester.pumpAndSettle();
    await tapRouteCounter('p1', tester);
    expect(findRoute('p1', count: 1), findsOneWidget);

    tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('r1');
    await tester.pumpAndSettle();
    await tapRouteCounter('r1', tester);
    expect(findRoute('r1', count: 1), findsOneWidget);

    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p2');
    await tester.pumpAndSettle();
    await tapRouteCounter('p2', tester);
    expect(findRoute('p2', count: 1), findsOneWidget);

    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).removePage('p1');
    await tester.pumpAndSettle();

    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('p1', count: 1, skipOffstage: false), findsNothing);
    expect(findRoute('r1', count: 1, skipOffstage: false), findsNothing);
    expect(findRoute('p2', count: 1), findsOneWidget);

    await tester.restartAndRestore();

    expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
    expect(findRoute('p1', count: 1, skipOffstage: false), findsNothing);
    expect(findRoute('r1', count: 1, skipOffstage: false), findsNothing);
    expect(findRoute('p2', count: 1), findsOneWidget);

    tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p1');
    await tester.pumpAndSettle();
    expect(findRoute('p1', count: 0), findsOneWidget);
  });

  testWidgetsWithLeakTracking('Helpful assert thrown all routes in onGenerateInitialRoutes are not restorable', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        restorationScopeId: 'material_app',
        initialRoute: '/',
        routes: <String, WidgetBuilder>{
          '/': (BuildContext context) => Container(),
        },
        onGenerateInitialRoutes: (String initialRoute) {
          return <MaterialPageRoute<void>>[
            MaterialPageRoute<void>(
              builder: (BuildContext context) => Container(),
            ),
          ];
        },
      ),
    );
    await tester.restartAndRestore();
    final dynamic exception = tester.takeException();
    expect(exception, isAssertionError);
    expect(
      (exception as AssertionError).message,
      contains('All routes returned by onGenerateInitialRoutes are not restorable.'),
    );

    // The previous assert leaves the widget tree in a broken state, so the
    // following code catches any remaining exceptions from attempting to build
    // new widget tree.
    final FlutterExceptionHandler? oldHandler = FlutterError.onError;
    dynamic remainingException;
    FlutterError.onError = (FlutterErrorDetails details) {
      remainingException ??= details.exception;
    };
    await tester.pumpWidget(Container(key: UniqueKey()));
    FlutterError.onError = oldHandler;
    expect(remainingException, isAssertionError);
  });
}

@pragma('vm:entry-point')
Route<void> _routeBuilder(BuildContext context, Object? arguments) {
  return MaterialPageRoute<void>(
    builder: (BuildContext context) {
      return RouteWidget(
        name: arguments! as String,
      );
    },
  );
}

@pragma('vm:entry-point')
Route<void> _routeFutureBuilder(BuildContext context, Object? arguments) {
  return MaterialPageRoute<void>(
    builder: (BuildContext context) {
      return const RouteFutureWidget();
    },
  );
}

class PagedTestWidget extends StatelessWidget {
  const PagedTestWidget({super.key, this.restorationId = 'app'});

  final String restorationId;

  @override
  Widget build(BuildContext context) {
    return RootRestorationScope(
      restorationId: restorationId,
      child: Directionality(
        textDirection: TextDirection.ltr,
        child: MediaQuery(
          data: MediaQueryData.fromView(View.of(context)),
          child: const PagedTestNavigator(),
        ),
      ),
    );
  }
}

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

  @override
  State<PagedTestNavigator> createState() => PagedTestNavigatorState();
}

class PagedTestNavigatorState extends State<PagedTestNavigator> with RestorationMixin {
  final RestorableString _routes = RestorableString('r-home');

  void addPage(String name, {bool restoreState = true, int? index}) {
    assert(!name.contains(','));
    assert(!name.startsWith('r-'));
    final List<String> routes = _routes.value.split(',');
    name = restoreState ? 'r-$name' : name;
    if (index != null) {
      routes.insert(index, name);
    } else {
      routes.add(name);
    }
    setState(() {
      _routes.value = routes.join(',');
    });
  }

  bool removePage(String name) {
    final List<String> routes = _routes.value.split(',');
    if (routes.remove(name) || routes.remove('r-$name')) {
      setState(() {
        _routes.value = routes.join(',');
      });
      return true;
    }
    return false;
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      restorationScopeId: 'nav',
      onPopPage: (Route<dynamic> route, dynamic result) {
        if (route.didPop(result)) {
          removePage(route.settings.name!);
          return true;
        }
        return false;
      },
      pages: _routes.value.isEmpty ? const <Page<Object?>>[] : _routes.value.split(',').map((String name) {
        if (name.startsWith('r-')) {
          name = name.substring(2);
          return TestPage(
            name: name,
            restorationId: name,
            key: ValueKey<String>(name),
          );
        }
        return TestPage(
          name: name,
          key: ValueKey<String>(name),
        );
      }).toList(),
      onGenerateRoute: (RouteSettings settings) {
        return MaterialPageRoute<int>(
          settings: settings,
          builder: (BuildContext context) {
            return RouteWidget(
              name: settings.name!,
              arguments: settings.arguments,
            );
          },
        );
      },
    );
  }

  @override
  String get restorationId => 'router';

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(_routes, 'routes');
  }

  @override
  void dispose() {
    super.dispose();
    _routes.dispose();
  }
}

class TestPage extends Page<void> {
  const TestPage({super.key, required String super.name, super.restorationId});

  @override
  Route<void> createRoute(BuildContext context) {
    return MaterialPageRoute<void>(
      settings: this,
      builder: (BuildContext context) {
        return RouteWidget(
          name: name!,
        );
      },
    );
  }
}

class TestWidget extends StatelessWidget {
  const TestWidget({super.key, this.restorationId = 'app'});

  final String? restorationId;

  @override
  Widget build(BuildContext context) {
    return RootRestorationScope(
      restorationId: restorationId,
      child: Directionality(
        textDirection: TextDirection.ltr,
        child: MediaQuery(
          data: MediaQueryData.fromView(View.of(context)),
          child: Navigator(
            initialRoute: 'home',
            restorationScopeId: 'app',
            onGenerateRoute: (RouteSettings settings) {
              return MaterialPageRoute<int>(
                settings: settings,
                builder: (BuildContext context) {
                  return RouteWidget(
                    name: settings.name!,
                    arguments: settings.arguments,
                  );
                },
              );
            },
          ),
        ),
      ),
    );
  }
}

class RouteWidget extends StatefulWidget {
  const RouteWidget({super.key, required this.name, this.arguments});

  final String name;
  final Object? arguments;

  @override
  State<RouteWidget> createState() => RouteWidgetState();
}

class RouteWidgetState extends State<RouteWidget> with RestorationMixin {
  final RestorableInt counter = RestorableInt(0);

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(counter, 'counter');
  }

  @override
  String get restorationId => 'stateful';

  @override
  void dispose() {
    super.dispose();
    counter.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: <Widget>[
          GestureDetector(
            child: Text('Route: ${widget.name}'),
            onTap: () {
              setState(() {
                counter.value++;
              });
            },
          ),
          if (widget.arguments != null)
            Text('Arguments(home): ${widget.arguments}'),
          Text('Counter(${widget.name}): ${counter.value}'),
        ],
      ),
    );
  }
}

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

  @override
  State<RouteFutureWidget> createState() => RouteFutureWidgetState();
}

class RouteFutureWidgetState extends State<RouteFutureWidget> with RestorationMixin {
  late RestorableRouteFuture<int> routeFuture;
  int? value;

  @override
  void initState() {
    super.initState();
    routeFuture = RestorableRouteFuture<int>(
      onPresent: (NavigatorState navigatorState, Object? arguments) {
        return navigatorState.restorablePushNamed(arguments! as String);
      },
      onComplete: (int i) {
        setState(() {
          value = i;
        });
      },
    );
  }

  @override
  void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
    registerForRestoration(routeFuture, 'routeFuture');
  }

  @override
  String get restorationId => 'routefuturewidget';

  @override
  void dispose() {
    super.dispose();
    routeFuture.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('Return value: $value'),
    );
  }
}

Finder findRoute(String name, { Object? arguments, int? count, bool skipOffstage = true }) => _RouteFinder(name, arguments: arguments, count: count, skipOffstage: skipOffstage);

Future<void> tapRouteCounter(String name, WidgetTester tester) async {
  await tester.tap(find.text('Route: $name'));
  await tester.pump();
}

class _RouteFinder extends MatchFinder {
  _RouteFinder(this.name, { this.arguments, this.count, super.skipOffstage });

  final String name;
  final Object? arguments;
  final int? count;

  @override
  String get description {
    String result = 'Route(name: $name';
    if (arguments != null) {
      result += ', arguments: $arguments';
    }
    if (count != null) {
      result += ', count: $count';
    }
    return result;
  }

  @override
  bool matches(Element candidate) {
    final Widget widget = candidate.widget;
    if (widget is RouteWidget) {
      if (widget.name != name) {
        return false;
      }
      if (arguments != null && widget.arguments != arguments) {
        return false;
      }
      final RouteWidgetState state = (candidate as StatefulElement).state as RouteWidgetState;
      if (count != null && state.counter.value != count) {
        return false;
      }
      return true;
    }
    return false;
  }
}

class FakeRoute extends Fake implements Route<void> { }
