// Copyright 2013 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/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:go_router/src/misc/error_screen.dart';

import 'test_helpers.dart';

Future<GoRouter> createGoRouter(
  WidgetTester tester, {
  Listenable? refreshListenable,
  bool dispose = true,
}) async {
  final GoRouter router = GoRouter(
    initialLocation: '/',
    routes: <GoRoute>[
      GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
      GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()),
      GoRoute(
        path: '/error',
        builder: (_, __) => const ErrorScreen(null),
      ),
    ],
    refreshListenable: refreshListenable,
  );
  if (dispose) {
    addTearDown(router.dispose);
  }
  await tester.pumpWidget(MaterialApp.router(
    routerConfig: router,
  ));
  return router;
}

Future<GoRouter> createGoRouterWithStatefulShellRoute(
    WidgetTester tester) async {
  final GoRouter router = GoRouter(
    initialLocation: '/',
    routes: <RouteBase>[
      GoRoute(path: '/', builder: (_, __) => const DummyStatefulWidget()),
      GoRoute(path: '/a', builder: (_, __) => const DummyStatefulWidget()),
      StatefulShellRoute.indexedStack(branches: <StatefulShellBranch>[
        StatefulShellBranch(routes: <RouteBase>[
          GoRoute(
              path: '/c',
              builder: (_, __) => const DummyStatefulWidget(),
              routes: <RouteBase>[
                GoRoute(
                    path: 'c1',
                    builder: (_, __) => const DummyStatefulWidget()),
                GoRoute(
                    path: 'c2',
                    builder: (_, __) => const DummyStatefulWidget()),
              ]),
        ]),
        StatefulShellBranch(routes: <RouteBase>[
          GoRoute(
              path: '/d',
              builder: (_, __) => const DummyStatefulWidget(),
              routes: <RouteBase>[
                GoRoute(
                    path: 'd1',
                    builder: (_, __) => const DummyStatefulWidget()),
              ]),
        ]),
      ], builder: mockStackedShellBuilder),
    ],
  );
  addTearDown(router.dispose);
  await tester.pumpWidget(MaterialApp.router(
    routerConfig: router,
  ));
  return router;
}

Future<GoRouter> createGoRouterWithStatefulShellRouteAndPopScopes(
  WidgetTester tester, {
  bool canPopShellRouteBuilder = true,
  bool canPopBranch = true,
  bool canPopBranchSubRoute = true,
  PopInvokedWithResultCallback<bool>? onPopShellRouteBuilder,
  PopInvokedWithResultCallback<bool>? onPopBranch,
  PopInvokedWithResultCallback<bool>? onPopBranchSubRoute,
}) async {
  final GoRouter router = GoRouter(
    initialLocation: '/c',
    routes: <RouteBase>[
      StatefulShellRoute.indexedStack(
        branches: <StatefulShellBranch>[
          StatefulShellBranch(routes: <RouteBase>[
            GoRoute(
                path: '/c',
                builder: (_, __) => PopScope(
                    onPopInvokedWithResult: onPopBranch,
                    canPop: canPopBranch,
                    child: const Text('Home')),
                routes: <RouteBase>[
                  GoRoute(
                    path: 'c1',
                    builder: (_, __) => PopScope(
                      onPopInvokedWithResult: onPopBranchSubRoute,
                      canPop: canPopBranchSubRoute,
                      child: const Text('SubRoute'),
                    ),
                  ),
                ]),
          ]),
        ],
        builder: (BuildContext context, GoRouterState state,
                StatefulNavigationShell navigationShell) =>
            PopScope(
          onPopInvokedWithResult: onPopShellRouteBuilder,
          canPop: canPopShellRouteBuilder,
          child: navigationShell,
        ),
      ),
    ],
  );

  addTearDown(router.dispose);
  await tester.pumpWidget(MaterialApp.router(
    routerConfig: router,
  ));
  return router;
}

void main() {
  group('pop', () {
    testWidgets('restore() update currentConfiguration in pop()',
        (WidgetTester tester) async {
      final ValueNotifier<int> valueNotifier = ValueNotifier<int>(0);
      final GoRouter goRouter = await createGoRouter(tester,
          refreshListenable: valueNotifier, dispose: false);

      goRouter.push('/a');
      await tester.pumpAndSettle();

      goRouter.pop();
      valueNotifier.notifyListeners();
      await tester.pumpAndSettle();
      expect(
        goRouter
            .routerDelegate.currentConfiguration.matches.last.matchedLocation,
        '/',
      );

      addTearDown(valueNotifier.dispose);
      addTearDown(goRouter.dispose);
    });

    testWidgets('removes the last element', (WidgetTester tester) async {
      final GoRouter goRouter = await createGoRouter(tester)
        ..push('/error');
      await tester.pumpAndSettle();
      expect(find.byType(ErrorScreen), findsOneWidget);
      final RouteMatchBase last =
          goRouter.routerDelegate.currentConfiguration.matches.last;
      await goRouter.routerDelegate.popRoute();
      expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
      expect(
          goRouter.routerDelegate.currentConfiguration.matches.contains(last),
          false);
    });

    testWidgets('PopScope intercepts back button on root route',
        (WidgetTester tester) async {
      bool didPop = false;

      final GoRouter goRouter = GoRouter(
        initialLocation: '/',
        routes: <GoRoute>[
          GoRoute(
            path: '/',
            builder: (_, __) => PopScope(
              onPopInvokedWithResult: (bool result, _) {
                didPop = true;
              },
              canPop: false,
              child: const Text('Home'),
            ),
          ),
        ],
      );

      addTearDown(goRouter.dispose);

      await tester.pumpWidget(MaterialApp.router(
        routerConfig: goRouter,
      ));

      expect(find.text('Home'), findsOneWidget);

      // Simulate back button press
      await tester.binding.handlePopRoute();

      await tester.pumpAndSettle();

      // Verify that PopScope intercepted the back button
      expect(didPop, isTrue);
      expect(find.text('Home'), findsOneWidget);
    });

    testWidgets(
        'PopScope intercepts back button on StatefulShellRoute builder route',
        (WidgetTester tester) async {
      bool didPopShellRouteBuilder = false;
      bool didPopBranch = false;
      bool didPopBranchSubRoute = false;

      await createGoRouterWithStatefulShellRouteAndPopScopes(
        tester,
        canPopShellRouteBuilder: false,
        onPopShellRouteBuilder: (_, __) => didPopShellRouteBuilder = true,
        onPopBranch: (_, __) => didPopBranch = true,
        onPopBranchSubRoute: (_, __) => didPopBranchSubRoute = true,
      );

      expect(find.text('Home'), findsOneWidget);
      await tester.binding.handlePopRoute();
      await tester.pumpAndSettle();

      // Verify that PopScope intercepted the back button
      expect(didPopShellRouteBuilder, isTrue);
      expect(didPopBranch, isFalse);
      expect(didPopBranchSubRoute, isFalse);

      expect(find.text('Home'), findsOneWidget);
    });

    testWidgets(
        'PopScope intercepts back button on StatefulShellRoute branch route',
        (WidgetTester tester) async {
      bool didPopShellRouteBuilder = false;
      bool didPopBranch = false;
      bool didPopBranchSubRoute = false;

      await createGoRouterWithStatefulShellRouteAndPopScopes(
        tester,
        canPopBranch: false,
        onPopShellRouteBuilder: (_, __) => didPopShellRouteBuilder = true,
        onPopBranch: (_, __) => didPopBranch = true,
        onPopBranchSubRoute: (_, __) => didPopBranchSubRoute = true,
      );

      expect(find.text('Home'), findsOneWidget);
      await tester.binding.handlePopRoute();
      await tester.pumpAndSettle();

      // Verify that PopScope intercepted the back button
      expect(didPopShellRouteBuilder, isFalse);
      expect(didPopBranch, isTrue);
      expect(didPopBranchSubRoute, isFalse);

      expect(find.text('Home'), findsOneWidget);
    });

    testWidgets(
        'PopScope intercepts back button on StatefulShellRoute branch sub route',
        (WidgetTester tester) async {
      bool didPopShellRouteBuilder = false;
      bool didPopBranch = false;
      bool didPopBranchSubRoute = false;

      final GoRouter goRouter =
          await createGoRouterWithStatefulShellRouteAndPopScopes(
        tester,
        canPopBranchSubRoute: false,
        onPopShellRouteBuilder: (_, __) => didPopShellRouteBuilder = true,
        onPopBranch: (_, __) => didPopBranch = true,
        onPopBranchSubRoute: (_, __) => didPopBranchSubRoute = true,
      );

      goRouter.push('/c/c1');
      await tester.pumpAndSettle();

      expect(find.text('SubRoute'), findsOneWidget);
      await tester.binding.handlePopRoute();
      await tester.pumpAndSettle();

      // Verify that PopScope intercepted the back button
      expect(didPopShellRouteBuilder, isFalse);
      expect(didPopBranch, isFalse);
      expect(didPopBranchSubRoute, isTrue);

      expect(find.text('SubRoute'), findsOneWidget);
    });

    testWidgets('pops more than matches count should return false',
        (WidgetTester tester) async {
      final GoRouter goRouter = await createGoRouter(tester)
        ..push('/error');
      await tester.pumpAndSettle();
      await goRouter.routerDelegate.popRoute();
      expect(await goRouter.routerDelegate.popRoute(), isFalse);
    });

    testWidgets('throw if nothing to pop', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> rootKey = GlobalKey<NavigatorState>();
      final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
      final GoRouter goRouter = await createRouter(
        <RouteBase>[
          ShellRoute(
            navigatorKey: rootKey,
            builder: (_, __, Widget child) => child,
            routes: <RouteBase>[
              ShellRoute(
                parentNavigatorKey: rootKey,
                navigatorKey: navKey,
                builder: (_, __, Widget child) => child,
                routes: <RouteBase>[
                  GoRoute(
                    path: '/',
                    parentNavigatorKey: navKey,
                    builder: (_, __) => const Text('Home'),
                  ),
                ],
              ),
            ],
          ),
        ],
        tester,
      );
      await tester.pumpAndSettle();
      expect(find.text('Home'), findsOneWidget);
      String? message;
      try {
        goRouter.pop();
      } on GoError catch (e) {
        message = e.message;
      }
      expect(message, 'There is nothing to pop');
    });

    testWidgets('poproute return false if nothing to pop',
        (WidgetTester tester) async {
      final GlobalKey<NavigatorState> rootKey = GlobalKey<NavigatorState>();
      final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
      final GoRouter goRouter = await createRouter(
        <RouteBase>[
          ShellRoute(
            navigatorKey: rootKey,
            builder: (_, __, Widget child) => child,
            routes: <RouteBase>[
              ShellRoute(
                parentNavigatorKey: rootKey,
                navigatorKey: navKey,
                builder: (_, __, Widget child) => child,
                routes: <RouteBase>[
                  GoRoute(
                    path: '/',
                    parentNavigatorKey: navKey,
                    builder: (_, __) => const Text('Home'),
                  ),
                ],
              ),
            ],
          ),
        ],
        tester,
      );
      expect(await goRouter.routerDelegate.popRoute(), isFalse);
    });
  });

  group('push', () {
    testWidgets(
      'It should return different pageKey when push is called',
      (WidgetTester tester) async {
        final GoRouter goRouter = await createGoRouter(tester);
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);

        goRouter.push('/a');
        await tester.pumpAndSettle();

        goRouter.push('/a');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
          isNot(equals(
              goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
        );
      },
    );

    testWidgets(
      'It should successfully push a route from outside the the current '
      'StatefulShellRoute',
      (WidgetTester tester) async {
        final GoRouter goRouter =
            await createGoRouterWithStatefulShellRoute(tester);
        goRouter.push('/c/c1');
        await tester.pumpAndSettle();
        goRouter.push('/a');
        await tester.pumpAndSettle();
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
          isNot(equals(
              goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
        );
      },
    );

    testWidgets(
      'It should successfully push a route that is a descendant of the current '
      'StatefulShellRoute branch',
      (WidgetTester tester) async {
        final GoRouter goRouter =
            await createGoRouterWithStatefulShellRoute(tester);
        goRouter.push('/c/c1');
        await tester.pumpAndSettle();

        goRouter.push('/c/c2');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        final ShellRouteMatch shellRouteMatch = goRouter.routerDelegate
            .currentConfiguration.matches.last as ShellRouteMatch;
        expect(shellRouteMatch.matches.length, 2);
        expect(
          shellRouteMatch.matches[0].pageKey,
          isNot(equals(shellRouteMatch.matches[1].pageKey)),
        );
      },
    );

    testWidgets(
      'It should successfully push the root of the current StatefulShellRoute '
      'branch upon itself',
      (WidgetTester tester) async {
        final GoRouter goRouter =
            await createGoRouterWithStatefulShellRoute(tester);
        goRouter.push('/c');
        await tester.pumpAndSettle();

        goRouter.push('/c');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        final ShellRouteMatch shellRouteMatch = goRouter.routerDelegate
            .currentConfiguration.matches.last as ShellRouteMatch;
        expect(shellRouteMatch.matches.length, 2);
        expect(
          shellRouteMatch.matches[0].pageKey,
          isNot(equals(shellRouteMatch.matches[1].pageKey)),
        );
      },
    );
  });

  group('canPop', () {
    testWidgets(
      'It should return false if there is only 1 match in the stack',
      (WidgetTester tester) async {
        final GoRouter goRouter = await createGoRouter(tester);

        await tester.pumpAndSettle();
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
        expect(goRouter.routerDelegate.canPop(), false);
      },
    );
    testWidgets(
      'It should return true if there is more than 1 match in the stack',
      (WidgetTester tester) async {
        final GoRouter goRouter = await createGoRouter(tester)
          ..push('/a');

        await tester.pumpAndSettle();
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        expect(goRouter.routerDelegate.canPop(), true);
      },
    );
    testWidgets(
      'It should return false if there are no matches in the stack',
      (WidgetTester tester) async {
        final GoRouter goRouter = GoRouter(
          initialLocation: '/',
          routes: <GoRoute>[],
        );
        addTearDown(goRouter.dispose);
        await tester.pumpWidget(MaterialApp.router(routerConfig: goRouter));
        await tester.pumpAndSettle();
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 0);
        expect(goRouter.routerDelegate.canPop(), false);
      },
    );
  });

  group('pushReplacement', () {
    testWidgets('It should replace the last match with the given one',
        (WidgetTester tester) async {
      final GoRouter goRouter = GoRouter(
        initialLocation: '/',
        routes: <GoRoute>[
          GoRoute(path: '/', builder: (_, __) => const SizedBox()),
          GoRoute(path: '/page-0', builder: (_, __) => const SizedBox()),
          GoRoute(path: '/page-1', builder: (_, __) => const SizedBox()),
        ],
      );
      addTearDown(goRouter.dispose);
      await tester.pumpWidget(
        MaterialApp.router(
          routerConfig: goRouter,
        ),
      );

      goRouter.push('/page-0');

      goRouter.routerDelegate.addListener(expectAsync0(() {}));
      final RouteMatchBase first =
          goRouter.routerDelegate.currentConfiguration.matches.first;
      final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
      goRouter.pushReplacement('/page-1');
      expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
      expect(
        goRouter.routerDelegate.currentConfiguration.matches.first,
        first,
        reason: 'The first match should still be in the list of matches',
      );
      expect(
        goRouter.routerDelegate.currentConfiguration.last,
        isNot(last),
        reason: 'The last match should have been removed',
      );
      expect(
        (goRouter.routerDelegate.currentConfiguration.last
                as ImperativeRouteMatch)
            .matches
            .uri
            .toString(),
        '/page-1',
        reason: 'The new location should have been pushed',
      );
    });

    testWidgets(
      'It should return different pageKey when pushReplacement is called',
      (WidgetTester tester) async {
        final GoRouter goRouter = await createGoRouter(tester);
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
          isNotNull,
        );

        goRouter.push('/a');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        final ValueKey<String> prev =
            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;

        goRouter.pushReplacement('/a');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
          isNot(equals(prev)),
        );
      },
    );
  });

  group('pushReplacementNamed', () {
    testWidgets(
      'It should replace the last match with the given one',
      (WidgetTester tester) async {
        final GoRouter goRouter = GoRouter(
          initialLocation: '/',
          routes: <GoRoute>[
            GoRoute(path: '/', builder: (_, __) => const SizedBox()),
            GoRoute(
                path: '/page-0',
                name: 'page0',
                builder: (_, __) => const SizedBox()),
            GoRoute(
                path: '/page-1',
                name: 'page1',
                builder: (_, __) => const SizedBox()),
          ],
        );
        addTearDown(goRouter.dispose);
        await tester.pumpWidget(
          MaterialApp.router(
            routerConfig: goRouter,
          ),
        );

        goRouter.pushNamed('page0');

        goRouter.routerDelegate.addListener(expectAsync0(() {}));
        final RouteMatchBase first =
            goRouter.routerDelegate.currentConfiguration.matches.first;
        final RouteMatch last =
            goRouter.routerDelegate.currentConfiguration.last;
        goRouter.pushReplacementNamed('page1');
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches.first,
          first,
          reason: 'The first match should still be in the list of matches',
        );
        expect(
          goRouter.routerDelegate.currentConfiguration.last,
          isNot(last),
          reason: 'The last match should have been removed',
        );
        expect(
          goRouter.routerDelegate.currentConfiguration.last,
          isA<RouteMatch>().having(
            (RouteMatch match) => match.route.name,
            'match.route.name',
            'page1',
          ),
          reason: 'The new location should have been pushed',
        );
      },
    );
  });

  group('replace', () {
    testWidgets('It should replace the last match with the given one',
        (WidgetTester tester) async {
      final GoRouter goRouter = GoRouter(
        initialLocation: '/',
        routes: <GoRoute>[
          GoRoute(path: '/', builder: (_, __) => const SizedBox()),
          GoRoute(path: '/page-0', builder: (_, __) => const SizedBox()),
          GoRoute(path: '/page-1', builder: (_, __) => const SizedBox()),
        ],
      );
      addTearDown(goRouter.dispose);
      await tester.pumpWidget(
        MaterialApp.router(
          routerConfig: goRouter,
        ),
      );

      goRouter.push('/page-0');

      goRouter.routerDelegate.addListener(expectAsync0(() {}));
      final RouteMatchBase first =
          goRouter.routerDelegate.currentConfiguration.matches.first;
      final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
      goRouter.replace<void>('/page-1');
      expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
      expect(
        goRouter.routerDelegate.currentConfiguration.matches.first,
        first,
        reason: 'The first match should still be in the list of matches',
      );
      expect(
        goRouter.routerDelegate.currentConfiguration.last,
        isNot(last),
        reason: 'The last match should have been removed',
      );
      expect(
        (goRouter.routerDelegate.currentConfiguration.last
                as ImperativeRouteMatch)
            .matches
            .uri
            .toString(),
        '/page-1',
        reason: 'The new location should have been pushed',
      );
    });

    testWidgets(
      'It should use the same pageKey when replace is called (with the same path)',
      (WidgetTester tester) async {
        final GoRouter goRouter = await createGoRouter(tester);
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
          isNotNull,
        );

        goRouter.push('/a');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        final ValueKey<String> prev =
            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;

        goRouter.replace<void>('/a');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
          prev,
        );
      },
    );

    testWidgets(
      'It should use the same pageKey when replace is called (with a different path)',
      (WidgetTester tester) async {
        final GoRouter goRouter = await createGoRouter(tester);
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches[0].pageKey,
          isNotNull,
        );

        goRouter.push('/a');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        final ValueKey<String> prev =
            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;

        goRouter.replace<void>('/');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
          prev,
        );
      },
    );
  });

  group('replaceNamed', () {
    Future<GoRouter> createGoRouter(
      WidgetTester tester, {
      Listenable? refreshListenable,
    }) async {
      final GoRouter router = GoRouter(
        initialLocation: '/',
        routes: <GoRoute>[
          GoRoute(
            path: '/',
            name: 'home',
            builder: (_, __) => const SizedBox(),
          ),
          GoRoute(
            path: '/page-0',
            name: 'page0',
            builder: (_, __) => const SizedBox(),
          ),
          GoRoute(
            path: '/page-1',
            name: 'page1',
            builder: (_, __) => const SizedBox(),
          ),
        ],
      );
      addTearDown(router.dispose);
      await tester.pumpWidget(MaterialApp.router(
        routerConfig: router,
      ));
      return router;
    }

    testWidgets('It should replace the last match with the given one',
        (WidgetTester tester) async {
      final GoRouter goRouter = await createGoRouter(tester);

      goRouter.pushNamed('page0');

      goRouter.routerDelegate.addListener(expectAsync0(() {}));
      final RouteMatchBase first =
          goRouter.routerDelegate.currentConfiguration.matches.first;
      final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
      goRouter.replaceNamed<void>('page1');
      expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
      expect(
        goRouter.routerDelegate.currentConfiguration.matches.first,
        first,
        reason: 'The first match should still be in the list of matches',
      );
      expect(
        goRouter.routerDelegate.currentConfiguration.last,
        isNot(last),
        reason: 'The last match should have been removed',
      );
      expect(
        (goRouter.routerDelegate.currentConfiguration.last
                as ImperativeRouteMatch)
            .matches
            .uri
            .toString(),
        '/page-1',
        reason: 'The new location should have been pushed',
      );
    });

    testWidgets(
      'It should use the same pageKey when replace is called with the same path',
      (WidgetTester tester) async {
        final GoRouter goRouter = await createGoRouter(tester);
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches.first.pageKey,
          isNotNull,
        );

        goRouter.pushNamed('page0');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        final ValueKey<String> prev =
            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;

        goRouter.replaceNamed<void>('page0');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
          prev,
        );
      },
    );

    testWidgets(
      'It should use a new pageKey when replace is called with a different path',
      (WidgetTester tester) async {
        final GoRouter goRouter = await createGoRouter(tester);
        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches.first.pageKey,
          isNotNull,
        );

        goRouter.pushNamed('page0');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        final ValueKey<String> prev =
            goRouter.routerDelegate.currentConfiguration.matches.last.pageKey;

        goRouter.replaceNamed<void>('home');
        await tester.pumpAndSettle();

        expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
        expect(
          goRouter.routerDelegate.currentConfiguration.matches.last.pageKey,
          prev,
        );
      },
    );
  });

  testWidgets('dispose unsubscribes from refreshListenable',
      (WidgetTester tester) async {
    final FakeRefreshListenable refreshListenable = FakeRefreshListenable();
    addTearDown(refreshListenable.dispose);

    final GoRouter goRouter = await createGoRouter(
      tester,
      refreshListenable: refreshListenable,
      dispose: false,
    );
    await tester.pumpWidget(Container());
    goRouter.dispose();
    expect(refreshListenable.unsubscribed, true);
  });
}

class FakeRefreshListenable extends ChangeNotifier {
  bool unsubscribed = false;

  @override
  void removeListener(VoidCallback listener) {
    unsubscribed = true;
    super.removeListener(listener);
  }
}

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

  @override
  State<DummyStatefulWidget> createState() => _DummyStatefulWidgetState();
}

class _DummyStatefulWidgetState extends State<DummyStatefulWidget> {
  @override
  Widget build(BuildContext context) => Container();
}