// 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 'test_helpers.dart';

void main() {
  group('RouteBuilder', () {
    testWidgets('Builds GoRoute', (WidgetTester tester) async {
      final RouteConfiguration config = createRouteConfiguration(
        routes: <RouteBase>[
          GoRoute(
            path: '/',
            builder: (BuildContext context, GoRouterState state) {
              return _DetailsScreen();
            },
          ),
        ],
        redirectLimit: 10,
        topRedirect: (BuildContext context, GoRouterState state) {
          return null;
        },
        navigatorKey: GlobalKey<NavigatorState>(),
      );

      final RouteMatchList matches = RouteMatchList(
          matches: <RouteMatch>[
            RouteMatch(
              route: config.routes.first as GoRoute,
              matchedLocation: '/',
              pageKey: const ValueKey<String>('/'),
            ),
          ],
          uri: Uri.parse('/'),
          pathParameters: const <String, String>{});

      await tester.pumpWidget(
        _BuilderTestWidget(
          routeConfiguration: config,
          matches: matches,
        ),
      );

      expect(find.byType(_DetailsScreen), findsOneWidget);
    });

    testWidgets('Builds ShellRoute', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> shellNavigatorKey =
          GlobalKey<NavigatorState>();
      final RouteConfiguration config = createRouteConfiguration(
        routes: <RouteBase>[
          ShellRoute(
            navigatorKey: shellNavigatorKey,
            builder: (BuildContext context, GoRouterState state, Widget child) {
              return _DetailsScreen();
            },
            routes: <GoRoute>[
              GoRoute(
                path: '/',
                builder: (BuildContext context, GoRouterState state) {
                  return _DetailsScreen();
                },
              ),
            ],
          ),
        ],
        redirectLimit: 10,
        topRedirect: (BuildContext context, GoRouterState state) {
          return null;
        },
        navigatorKey: GlobalKey<NavigatorState>(),
      );

      final RouteMatchList matches = RouteMatchList(
        matches: <RouteMatchBase>[
          ShellRouteMatch(
            route: config.routes.first as ShellRouteBase,
            matchedLocation: '',
            pageKey: const ValueKey<String>(''),
            navigatorKey: shellNavigatorKey,
            matches: <RouteMatchBase>[
              RouteMatch(
                route: config.routes.first.routes.first as GoRoute,
                matchedLocation: '/',
                pageKey: const ValueKey<String>('/'),
              ),
            ],
          ),
        ],
        uri: Uri.parse('/'),
        pathParameters: const <String, String>{},
      );

      await tester.pumpWidget(
        _BuilderTestWidget(
          routeConfiguration: config,
          matches: matches,
        ),
      );

      expect(find.byType(_DetailsScreen), findsOneWidget);
    });

    testWidgets('Uses the correct navigatorKey', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> rootNavigatorKey =
          GlobalKey<NavigatorState>();
      final RouteConfiguration config = createRouteConfiguration(
        navigatorKey: rootNavigatorKey,
        routes: <RouteBase>[
          GoRoute(
            path: '/',
            builder: (BuildContext context, GoRouterState state) {
              return _DetailsScreen();
            },
          ),
        ],
        redirectLimit: 10,
        topRedirect: (BuildContext context, GoRouterState state) {
          return null;
        },
      );

      final RouteMatchList matches = RouteMatchList(
          matches: <RouteMatch>[
            RouteMatch(
              route: config.routes.first as GoRoute,
              matchedLocation: '/',
              pageKey: const ValueKey<String>('/'),
            ),
          ],
          uri: Uri.parse('/'),
          pathParameters: const <String, String>{});

      await tester.pumpWidget(
        _BuilderTestWidget(
          routeConfiguration: config,
          matches: matches,
        ),
      );

      expect(find.byKey(rootNavigatorKey), findsOneWidget);
    });

    testWidgets('Builds a Navigator for ShellRoute',
        (WidgetTester tester) async {
      final GlobalKey<NavigatorState> rootNavigatorKey =
          GlobalKey<NavigatorState>(debugLabel: 'root');
      final GlobalKey<NavigatorState> shellNavigatorKey =
          GlobalKey<NavigatorState>(debugLabel: 'shell');
      final RouteConfiguration config = createRouteConfiguration(
        navigatorKey: rootNavigatorKey,
        routes: <RouteBase>[
          ShellRoute(
            builder: (BuildContext context, GoRouterState state, Widget child) {
              return _HomeScreen(
                child: child,
              );
            },
            navigatorKey: shellNavigatorKey,
            routes: <RouteBase>[
              GoRoute(
                path: '/details',
                builder: (BuildContext context, GoRouterState state) {
                  return _DetailsScreen();
                },
              ),
            ],
          ),
        ],
        redirectLimit: 10,
        topRedirect: (BuildContext context, GoRouterState state) {
          return null;
        },
      );

      final RouteMatchList matches = RouteMatchList(
          matches: <RouteMatchBase>[
            ShellRouteMatch(
                route: config.routes.first as ShellRouteBase,
                matchedLocation: '',
                pageKey: const ValueKey<String>(''),
                navigatorKey: shellNavigatorKey,
                matches: <RouteMatchBase>[
                  RouteMatch(
                    route: config.routes.first.routes.first as GoRoute,
                    matchedLocation: '/details',
                    pageKey: const ValueKey<String>('/details'),
                  ),
                ]),
          ],
          uri: Uri.parse('/details'),
          pathParameters: const <String, String>{});

      await tester.pumpWidget(
        _BuilderTestWidget(
          routeConfiguration: config,
          matches: matches,
        ),
      );

      expect(find.byType(_HomeScreen, skipOffstage: false), findsOneWidget);
      expect(find.byType(_DetailsScreen), findsOneWidget);
      expect(find.byKey(rootNavigatorKey), findsOneWidget);
      expect(find.byKey(shellNavigatorKey), findsOneWidget);
    });

    testWidgets('Builds a Navigator for ShellRoute with parentNavigatorKey',
        (WidgetTester tester) async {
      final GlobalKey<NavigatorState> rootNavigatorKey =
          GlobalKey<NavigatorState>(debugLabel: 'root');
      final GlobalKey<NavigatorState> shellNavigatorKey =
          GlobalKey<NavigatorState>(debugLabel: 'shell');
      final RouteConfiguration config = createRouteConfiguration(
        navigatorKey: rootNavigatorKey,
        routes: <RouteBase>[
          ShellRoute(
            builder: (BuildContext context, GoRouterState state, Widget child) {
              return _HomeScreen(
                child: child,
              );
            },
            navigatorKey: shellNavigatorKey,
            routes: <RouteBase>[
              GoRoute(
                path: '/a',
                builder: (BuildContext context, GoRouterState state) {
                  return _DetailsScreen();
                },
                routes: <RouteBase>[
                  GoRoute(
                    path: 'details',
                    builder: (BuildContext context, GoRouterState state) {
                      return _DetailsScreen();
                    },
                    // This screen should stack onto the root navigator.
                    parentNavigatorKey: rootNavigatorKey,
                  ),
                ],
              ),
            ],
          ),
        ],
        redirectLimit: 10,
        topRedirect: (BuildContext context, GoRouterState state) {
          return null;
        },
      );

      final RouteMatchList matches = RouteMatchList(
          matches: <RouteMatch>[
            RouteMatch(
              route: config.routes.first.routes.first as GoRoute,
              matchedLocation: '/a/details',
              pageKey: const ValueKey<String>('/a/details'),
            ),
          ],
          uri: Uri.parse('/a/details'),
          pathParameters: const <String, String>{});

      await tester.pumpWidget(
        _BuilderTestWidget(
          routeConfiguration: config,
          matches: matches,
        ),
      );

      // The Details screen should be visible, but the HomeScreen should be
      // offstage (underneath) the DetailsScreen.
      expect(find.byType(_HomeScreen), findsNothing);
      expect(find.byType(_DetailsScreen), findsOneWidget);
    });

    testWidgets('Uses the correct restorationScopeId for ShellRoute',
        (WidgetTester tester) async {
      final GlobalKey<NavigatorState> rootNavigatorKey =
          GlobalKey<NavigatorState>(debugLabel: 'root');
      final GlobalKey<NavigatorState> shellNavigatorKey =
          GlobalKey<NavigatorState>(debugLabel: 'shell');
      final RouteConfiguration config = createRouteConfiguration(
        navigatorKey: rootNavigatorKey,
        routes: <RouteBase>[
          ShellRoute(
            builder: (BuildContext context, GoRouterState state, Widget child) {
              return _HomeScreen(child: child);
            },
            navigatorKey: shellNavigatorKey,
            restorationScopeId: 'scope1',
            routes: <RouteBase>[
              GoRoute(
                path: '/a',
                builder: (BuildContext context, GoRouterState state) {
                  return _DetailsScreen();
                },
              ),
            ],
          ),
        ],
        redirectLimit: 10,
        topRedirect: (BuildContext context, GoRouterState state) {
          return null;
        },
      );

      final RouteMatchList matches = RouteMatchList(
        matches: <RouteMatchBase>[
          ShellRouteMatch(
            route: config.routes.first as ShellRouteBase,
            matchedLocation: '',
            pageKey: const ValueKey<String>(''),
            navigatorKey: shellNavigatorKey,
            matches: <RouteMatchBase>[
              RouteMatch(
                route: config.routes.first.routes.first as GoRoute,
                matchedLocation: '/a',
                pageKey: const ValueKey<String>('/a'),
              ),
            ],
          ),
        ],
        uri: Uri.parse('/b'),
        pathParameters: const <String, String>{},
      );

      await tester.pumpWidget(
        _BuilderTestWidget(
          routeConfiguration: config,
          matches: matches,
        ),
      );

      expect(find.byKey(rootNavigatorKey), findsOneWidget);
      expect(find.byKey(shellNavigatorKey), findsOneWidget);
      expect(
          (shellNavigatorKey.currentWidget as Navigator?)?.restorationScopeId,
          'scope1');
    });

    testWidgets('Uses the correct restorationScopeId for StatefulShellRoute',
        (WidgetTester tester) async {
      final GlobalKey<NavigatorState> rootNavigatorKey =
          GlobalKey<NavigatorState>(debugLabel: 'root');
      final GlobalKey<NavigatorState> shellNavigatorKey =
          GlobalKey<NavigatorState>(debugLabel: 'shell');
      final GoRouter goRouter = GoRouter(
        initialLocation: '/a',
        navigatorKey: rootNavigatorKey,
        routes: <RouteBase>[
          StatefulShellRoute.indexedStack(
            restorationScopeId: 'shell',
            builder: (BuildContext context, GoRouterState state,
                    StatefulNavigationShell navigationShell) =>
                _HomeScreen(child: navigationShell),
            branches: <StatefulShellBranch>[
              StatefulShellBranch(
                navigatorKey: shellNavigatorKey,
                restorationScopeId: 'scope1',
                routes: <RouteBase>[
                  GoRoute(
                    path: '/a',
                    builder: (BuildContext context, GoRouterState state) {
                      return _DetailsScreen();
                    },
                  ),
                ],
              ),
            ],
          ),
        ],
      );
      addTearDown(goRouter.dispose);

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

      expect(find.byKey(rootNavigatorKey), findsOneWidget);
      expect(find.byKey(shellNavigatorKey), findsOneWidget);
      expect(
          (shellNavigatorKey.currentWidget as Navigator?)?.restorationScopeId,
          'scope1');
    });

    testWidgets('GoRouter requestFocus defaults to true',
        (WidgetTester tester) async {
      final GoRouter router = GoRouter(
        routes: <RouteBase>[
          GoRoute(
            path: '/',
            builder: (BuildContext context, GoRouterState state) =>
                const Scaffold(
              body: Center(child: Text('Home')),
            ),
          ),
        ],
      );

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

      addTearDown(() => router.dispose());

      final Navigator navigator =
          tester.widget<Navigator>(find.byType(Navigator));
      expect(navigator.requestFocus, isTrue);
    });

    testWidgets('GoRouter requestFocus can be set to false',
        (WidgetTester tester) async {
      final GoRouter router = GoRouter(
        routes: <RouteBase>[
          GoRoute(
            path: '/',
            builder: (BuildContext context, GoRouterState state) =>
                const Scaffold(
              body: Center(child: Text('Home')),
            ),
          ),
        ],
        requestFocus: false,
      );

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

      addTearDown(() => router.dispose());

      final Navigator navigator =
          tester.widget<Navigator>(find.byType(Navigator));
      expect(navigator.requestFocus, isFalse);
    });
  });
}

class _HomeScreen extends StatelessWidget {
  const _HomeScreen({
    required this.child,
  });

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          const Text('Home Screen'),
          Expanded(child: child),
        ],
      ),
    );
  }
}

class _DetailsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Text('Details Screen'),
    );
  }
}

class _BuilderTestWidget extends StatelessWidget {
  _BuilderTestWidget({
    required this.routeConfiguration,
    required this.matches,
  }) : builder = _routeBuilder(routeConfiguration);

  final RouteConfiguration routeConfiguration;
  final RouteBuilder builder;
  final RouteMatchList matches;

  /// Builds a [RouteBuilder] for tests
  static RouteBuilder _routeBuilder(RouteConfiguration configuration) {
    return RouteBuilder(
      configuration: configuration,
      builderWithNav: (
        BuildContext context,
        Widget child,
      ) {
        return child;
      },
      errorPageBuilder: (
        BuildContext context,
        GoRouterState state,
      ) {
        return MaterialPage<dynamic>(
          child: Text('Error: ${state.error}'),
        );
      },
      errorBuilder: (
        BuildContext context,
        GoRouterState state,
      ) {
        return Text('Error: ${state.error}');
      },
      restorationScopeId: null,
      observers: <NavigatorObserver>[],
      onPopPageWithRouteMatch: (_, __, ___) => false,
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: builder.build(context, matches, false),
    );
  }
}