// Copyright 2013 The Flutter Authors
// 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 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 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 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 = 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 {
      var didPop = false;

      final 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 {
        var didPopShellRouteBuilder = false;
        var didPopBranch = false;
        var 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 {
        var didPopShellRouteBuilder = false;
        var didPopBranch = false;
        var 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 {
        var didPopShellRouteBuilder = false;
        var didPopBranch = false;
        var 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 rootKey = GlobalKey<NavigatorState>();
      final 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 rootKey = GlobalKey<NavigatorState>();
      final 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 =
            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 =
            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(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(
        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(
        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(
        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 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 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();
}