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

import 'dart:async';
import 'dart:ui';

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

import '../foundation/leak_tracking.dart';

void main() {
  tearDown(() {
    LicenseRegistry.reset();
  });

  testWidgetsWithLeakTracking('Material3 has sentence case labels', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      theme: ThemeData(useMaterial3: true),
      builder: (BuildContext context, Widget? child) {
        return MediaQuery(
          // Display has a vertical hinge down the middle
          data: const MediaQueryData(
            size: Size(800, 600),
            displayFeatures: <DisplayFeature>[
              DisplayFeature(
                bounds: Rect.fromLTRB(390, 0, 410, 600),
                type: DisplayFeatureType.hinge,
                state: DisplayFeatureState.unknown,
              ),
            ],
          ),
          child: child!,
        );
      },
      home: Builder(
        builder: (BuildContext context) => ElevatedButton(
          onPressed: () {
            showAboutDialog(
              context: context,
              useRootNavigator: false,
              applicationName: 'A',
            );
          },
          child: const Text('Show About Dialog'),
        ),
      ),
    ));

    // Open the dialog.
    await tester.tap(find.byType(ElevatedButton));
    await tester.pumpAndSettle();
    expect(find.text('Close'), findsOneWidget);
    expect(find.text('View licenses'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('AboutListTile control test', (WidgetTester tester) async {
    const FlutterLogo logo = FlutterLogo();

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        title: 'Pirate app',
        home: Scaffold(
          appBar: AppBar(
            title: const Text('Home'),
          ),
          drawer: Drawer(
            child: ListView(
              children: const <Widget>[
                AboutListTile(
                  applicationVersion: '0.1.2',
                  applicationIcon: logo,
                  applicationLegalese: 'I am the very model of a modern major general.',
                  aboutBoxChildren: <Widget>[
                    Text('About box'),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(find.text('About Pirate app'), findsNothing);
    expect(find.text('0.1.2'), findsNothing);
    expect(find.byWidget(logo), findsNothing);
    expect(
      find.text('I am the very model of a modern major general.'),
      findsNothing,
    );
    expect(find.text('About box'), findsNothing);

    await tester.tap(find.byType(IconButton));
    await tester.pumpAndSettle();

    expect(find.text('About Pirate app'), findsOneWidget);
    expect(find.text('0.1.2'), findsNothing);
    expect(find.byWidget(logo), findsNothing);
    expect(
      find.text('I am the very model of a modern major general.'),
      findsNothing,
    );
    expect(find.text('About box'), findsNothing);

    await tester.tap(find.text('About Pirate app'));
    await tester.pumpAndSettle();

    expect(find.text('About Pirate app'), findsOneWidget);
    expect(find.text('0.1.2'), findsOneWidget);
    expect(find.byWidget(logo), findsOneWidget);
    expect(
      find.text('I am the very model of a modern major general.'),
      findsOneWidget,
    );
    expect(find.text('About box'), findsOneWidget);

    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['Pirate package '], 'Pirate license'),
      ]);
    });

    await tester.tap(find.text('VIEW LICENSES'));
    await tester.pumpAndSettle();

    expect(find.text('Pirate app'), findsOneWidget);
    expect(find.text('0.1.2'), findsOneWidget);
    expect(find.byWidget(logo), findsOneWidget);
    expect(
      find.text('I am the very model of a modern major general.'),
      findsOneWidget,
    );
    await tester.tap(find.text('Pirate package '));
    await tester.pumpAndSettle();
    expect(find.text('Pirate license'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('About box logic defaults to executable name for app name', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        title: 'flutter_tester',
        home: Material(child: AboutListTile()),
      ),
    );
    expect(find.text('About flutter_tester'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('LicensePage control test', (WidgetTester tester) async {
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'),
      ]);
    });

    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(
          <String>['Another package'],
          'Another license',
        ),
      ]);
    });

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: const Center(
          child: LicensePage(),
        ),
      ),
    );

    expect(find.text('AAA'), findsNothing);
    expect(find.text('BBB'), findsNothing);
    expect(find.text('Another package'), findsNothing);
    expect(find.text('Another license'), findsNothing);

    await tester.pumpAndSettle();

    // Check for packages.
    expect(find.text('AAA'), findsOneWidget);
    expect(find.text('Another package'), findsOneWidget);

    // Check license is displayed after entering into license page for 'AAA'.
    await tester.tap(find.text('AAA'));
    await tester.pumpAndSettle();
    expect(find.text('BBB'), findsOneWidget);

    /// Go back to list of packages.
    await tester.pageBack();
    await tester.pumpAndSettle();

    /// Check license is displayed after entering into license page for
    /// 'Another package'.
    await tester.tap(find.text('Another package'));
    await tester.pumpAndSettle();
    expect(find.text('Another license'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('LicensePage control test with all properties', (WidgetTester tester) async {
    const FlutterLogo logo = FlutterLogo();

    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'),
      ]);
    });

    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(
          <String>['Another package'],
          'Another license',
        ),
      ]);
    });

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        title: 'Pirate app',
        home: const Center(
          child: LicensePage(
            applicationName: 'LicensePage test app',
            applicationVersion: '0.1.2',
            applicationIcon: logo,
            applicationLegalese: 'I am the very model of a modern major general.',
          ),
        ),
      ),
    );

    expect(find.text('Pirate app'), findsNothing);
    expect(find.text('LicensePage test app'), findsOneWidget);
    expect(find.text('0.1.2'), findsOneWidget);
    expect(find.byWidget(logo), findsOneWidget);
    expect(
      find.text('I am the very model of a modern major general.'),
      findsOneWidget,
    );
    expect(find.text('AAA'), findsNothing);
    expect(find.text('BBB'), findsNothing);
    expect(find.text('Another package'), findsNothing);
    expect(find.text('Another license'), findsNothing);

    await tester.pumpAndSettle();

    expect(find.text('Pirate app'), findsNothing);
    expect(find.text('LicensePage test app'), findsOneWidget);
    expect(find.text('0.1.2'), findsOneWidget);
    expect(find.byWidget(logo), findsOneWidget);
    expect(
      find.text('I am the very model of a modern major general.'),
      findsOneWidget,
    );

    // Check for packages.
    expect(find.text('AAA'), findsOneWidget);
    expect(find.text('Another package'), findsOneWidget);

    // Check license is displayed after entering into license page for 'AAA'.
    await tester.tap(find.text('AAA'));
    await tester.pumpAndSettle();
    expect(find.text('BBB'), findsOneWidget);

    /// Go back to list of packages.
    await tester.pageBack();
    await tester.pumpAndSettle();

    /// Check license is displayed after entering into license page for
    /// 'Another package'.
    await tester.tap(find.text('Another package'));
    await tester.pumpAndSettle();
    expect(find.text('Another license'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('_PackageLicensePage title style without AppBarTheme', (WidgetTester tester) async {
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'),
      ]);
    });

    const TextStyle titleTextStyle = TextStyle(
      fontSize: 20,
      color: Colors.black,
      inherit: false,
    );
    const TextStyle subtitleTextStyle = TextStyle(
      fontSize: 15,
      color: Colors.red,
      inherit: false,
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(
          useMaterial3: false,
          primaryTextTheme: const TextTheme(
            titleLarge: titleTextStyle,
            titleSmall: subtitleTextStyle,
          ),
        ),
        home: const Center(
          child: LicensePage(),
        ),
      ),
    );
    await tester.pumpAndSettle();

    // Check for packages.
    expect(find.text('AAA'), findsOneWidget);

    // Check license is displayed after entering into license page for 'AAA'.
    await tester.tap(find.text('AAA'));
    await tester.pumpAndSettle();

    // Check for titles style.
    final Text title = tester.widget(find.text('AAA'));
    expect(title.style, titleTextStyle);
    final Text subtitle = tester.widget(find.text('1 license.'));
    expect(subtitle.style, subtitleTextStyle);
  });

  testWidgetsWithLeakTracking('_PackageLicensePage title style with AppBarTheme', (WidgetTester tester) async {
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'),
      ]);
    });

    const TextStyle titleTextStyle = TextStyle(
      fontSize: 20,
      color: Colors.indigo,
    );

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(
          // Not used because appBarTheme is prioritized.
          primaryTextTheme: const TextTheme(
            titleLarge: TextStyle(
              fontSize: 12,
              color: Colors.grey,
            ),
            titleSmall: TextStyle(
              fontSize: 10,
              color: Colors.grey,
            ),
          ),
          appBarTheme: const AppBarTheme(
            titleTextStyle: titleTextStyle,
            foregroundColor: Colors.indigo,
          ),
        ),
        home: const Center(
          child: LicensePage(),
        ),
      ),
    );
    await tester.pumpAndSettle();

    // Check for packages.
    expect(find.text('AAA'), findsOneWidget);

    // Check license is displayed after entering into license page for 'AAA'.
    await tester.tap(find.text('AAA'));
    await tester.pumpAndSettle();

    // Check for titles style.
    final Text title = tester.widget(find.text('AAA'));
    expect(title.style, titleTextStyle);
  });

  testWidgetsWithLeakTracking('LicensePage respects the notch', (WidgetTester tester) async {
    const double safeareaPadding = 27.0;

    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'),
      ]);
    });

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: const MediaQuery(
          data: MediaQueryData(
            padding: EdgeInsets.all(safeareaPadding),
          ),
          child: LicensePage(),
        ),
      ),
    );

    await tester.pumpAndSettle();

    // The position of the top left of app bar title should indicate whether
    // the safe area is sufficiently respected.
    expect(
      tester.getTopLeft(find.text('Licenses')),
      const Offset(16.0 + safeareaPadding, 18.0 + safeareaPadding),
    );
  });

  testWidgetsWithLeakTracking('LicensePage returns early if unmounted', (WidgetTester tester) async {
    final Completer<LicenseEntry> licenseCompleter = Completer<LicenseEntry>();
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromFuture(licenseCompleter.future);
    });

    await tester.pumpWidget(
      const MaterialApp(
        home: LicensePage(),
      ),
    );
    await tester.pump();

    await tester.pumpWidget(
      const MaterialApp(
        home: Placeholder(),
      ),
    );

    await tester.pumpAndSettle();
    final FakeLicenseEntry licenseEntry = FakeLicenseEntry();
    licenseCompleter.complete(licenseEntry);
    expect(licenseEntry.packagesCalled, false);
  });

  testWidgetsWithLeakTracking('LicensePage returns late if unmounted', (WidgetTester tester) async {
    final Completer<LicenseEntry> licenseCompleter = Completer<LicenseEntry>();
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromFuture(licenseCompleter.future);
    });

    await tester.pumpWidget(
      const MaterialApp(
        home: LicensePage(),
      ),
    );
    await tester.pump();
    final FakeLicenseEntry licenseEntry = FakeLicenseEntry();
    licenseCompleter.complete(licenseEntry);

    await tester.pumpWidget(
      const MaterialApp(
        home: Placeholder(),
      ),
    );

    await tester.pumpAndSettle();
    expect(licenseEntry.packagesCalled, true);
  });

  testWidgetsWithLeakTracking('LicensePage logic defaults to executable name for app name', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        title: 'flutter_tester',
        home: Material(child: LicensePage()),
      ),
    );
    expect(find.text('flutter_tester'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('AboutListTile dense property is applied', (WidgetTester tester) async {
    await tester.pumpWidget(const MaterialApp(
      home: Material(child: Center(child: AboutListTile())),
    ));
    Rect tileRect = tester.getRect(find.byType(AboutListTile));
    expect(tileRect.height, 56.0);

    await tester.pumpWidget(const MaterialApp(
      home: Material(child: Center(child: AboutListTile(dense: false))),
    ));
    tileRect = tester.getRect(find.byType(AboutListTile));
    expect(tileRect.height, 56.0);

    await tester.pumpWidget(const MaterialApp(
      home: Material(child: Center(child: AboutListTile(dense: true))),
    ));
    tileRect = tester.getRect(find.byType(AboutListTile));
    expect(tileRect.height, 48.0);
  });

  testWidgetsWithLeakTracking('showLicensePage uses nested navigator by default', (WidgetTester tester) async {
    final LicensePageObserver rootObserver = LicensePageObserver();
    final LicensePageObserver nestedObserver = LicensePageObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      initialRoute: '/',
      onGenerateRoute: (_) {
        return PageRouteBuilder<dynamic>(
          pageBuilder: (_, __, ___) => Navigator(
            observers: <NavigatorObserver>[nestedObserver],
            onGenerateRoute: (RouteSettings settings) {
              return PageRouteBuilder<dynamic>(
                pageBuilder: (BuildContext context, _, __) {
                  return ElevatedButton(
                    onPressed: () {
                      showLicensePage(
                        context: context,
                        applicationName: 'A',
                      );
                    },
                    child: const Text('Show License Page'),
                  );
                },
              );
            },
          ),
        );
      },
    ));

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

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

  testWidgetsWithLeakTracking('showLicensePage uses root navigator if useRootNavigator is true', (WidgetTester tester) async {
    final LicensePageObserver rootObserver = LicensePageObserver();
    final LicensePageObserver nestedObserver = LicensePageObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      initialRoute: '/',
      onGenerateRoute: (_) {
        return PageRouteBuilder<dynamic>(
          pageBuilder: (_, __, ___) => Navigator(
            observers: <NavigatorObserver>[nestedObserver],
            onGenerateRoute: (RouteSettings settings) {
              return PageRouteBuilder<dynamic>(
                pageBuilder: (BuildContext context, _, __) {
                  return ElevatedButton(
                    onPressed: () {
                      showLicensePage(
                        context: context,
                        useRootNavigator: true,
                        applicationName: 'A',
                      );
                    },
                    child: const Text('Show License Page'),
                  );
                },
              );
            },
          ),
        );
      },
    ));

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

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

  testWidgetsWithLeakTracking('showAboutDialog uses root navigator by default', (WidgetTester tester) async {
    final AboutDialogObserver rootObserver = AboutDialogObserver();
    final AboutDialogObserver nestedObserver = AboutDialogObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[nestedObserver],
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  showAboutDialog(
                    context: context,
                    applicationName: 'A',
                  );
                },
                child: const Text('Show About Dialog'),
              );
            },
          );
        },
      ),
    ));

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

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

  testWidgetsWithLeakTracking('showAboutDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
    final AboutDialogObserver rootObserver = AboutDialogObserver();
    final AboutDialogObserver nestedObserver = AboutDialogObserver();

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[nestedObserver],
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  showAboutDialog(
                    context: context,
                    useRootNavigator: false,
                    applicationName: 'A',
                  );
                },
                child: const Text('Show About Dialog'),
              );
            },
          );
        },
      ),
    ));

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

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

  group('showAboutDialog avoids overlapping display features', () {
    testWidgetsWithLeakTracking('default positioning', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            // Display has a vertical hinge down the middle
            data: const MediaQueryData(
              size: Size(800, 600),
              displayFeatures: <DisplayFeature>[
                DisplayFeature(
                  bounds: Rect.fromLTRB(390, 0, 410, 600),
                  type: DisplayFeatureType.hinge,
                  state: DisplayFeatureState.unknown,
                ),
              ],
            ),
            child: child!,
          );
        },
        home: Builder(
          builder: (BuildContext context) => ElevatedButton(
            onPressed: () {
              showAboutDialog(
                context: context,
                useRootNavigator: false,
                applicationName: 'A',
              );
            },
            child: const Text('Show About Dialog'),
          ),
        ),
      ));

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

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

    testWidgetsWithLeakTracking('positioning using anchorPoint', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            // Display has a vertical hinge down the middle
            data: const MediaQueryData(
              size: Size(800, 600),
              displayFeatures: <DisplayFeature>[
                DisplayFeature(
                  bounds: Rect.fromLTRB(390, 0, 410, 600),
                  type: DisplayFeatureType.hinge,
                  state: DisplayFeatureState.unknown,
                ),
              ],
            ),
            child: child!,
          );
        },
        home: Builder(
          builder: (BuildContext context) => ElevatedButton(
            onPressed: () {
              showAboutDialog(
                context: context,
                useRootNavigator: false,
                applicationName: 'A',
                anchorPoint: const Offset(1000, 0),
              );
            },
            child: const Text('Show About Dialog'),
          ),
        ),
      ));

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

      // The anchorPoint hits the right side of the display
      expect(tester.getTopLeft(find.byType(AboutDialog)), const Offset(410.0, 0.0));
      expect(tester.getBottomRight(find.byType(AboutDialog)), const Offset(800.0, 600.0));
    });

    testWidgetsWithLeakTracking('positioning using Directionality', (WidgetTester tester) async {
      await tester.pumpWidget(MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            // Display has a vertical hinge down the middle
            data: const MediaQueryData(
              size: Size(800, 600),
              displayFeatures: <DisplayFeature>[
                DisplayFeature(
                  bounds: Rect.fromLTRB(390, 0, 410, 600),
                  type: DisplayFeatureType.hinge,
                  state: DisplayFeatureState.unknown,
                ),
              ],
            ),
            child: Directionality(
              textDirection: TextDirection.rtl,
              child: child!,
            ),
          );
        },
        home: Builder(
          builder: (BuildContext context) => ElevatedButton(
            onPressed: () {
              showAboutDialog(
                context: context,
                useRootNavigator: false,
                applicationName: 'A',
              );
            },
            child: const Text('Show About Dialog'),
          ),
        ),
      ));

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

      // Since this is rtl, the first screen is the on the right
      expect(tester.getTopLeft(find.byType(AboutDialog)), const Offset(410.0, 0.0));
      expect(tester.getBottomRight(find.byType(AboutDialog)), const Offset(800.0, 600.0));
    });
  });

  testWidgetsWithLeakTracking("AboutListTile's child should not be offset when the icon is not specified.", (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(
          body: AboutListTile(
            child: Text('About'),
          ),
        ),
      ),
    );

    expect(
      find.descendant(
        of: find.byType(AboutListTile),
        matching: find.byType(Icon),
      ),
      findsNothing,
    );
  });

  testWidgetsWithLeakTracking("AboutDialog's contents are scrollable", (WidgetTester tester) async {
    final Key contentKey = UniqueKey();
    await tester.pumpWidget(MaterialApp(
      home: Navigator(
        onGenerateRoute: (RouteSettings settings) {
          return MaterialPageRoute<dynamic>(
            builder: (BuildContext context) {
              return ElevatedButton(
                onPressed: () {
                  showAboutDialog(
                    context: context,
                    useRootNavigator: false,
                    applicationName: 'A',
                    children: <Widget>[
                      Container(
                        key: contentKey,
                        color: Colors.orange,
                        height: 500,
                      ),
                    ],
                  );
                },
                child: const Text('Show About Dialog'),
              );
            },
          );
        },
      ),
    ));

    await tester.tap(find.text('Show About Dialog'));
    await tester.pumpAndSettle();

    // Try dragging by the [AboutDialog]'s title.
    RenderBox box = tester.renderObject(find.text('A'));
    Offset originalOffset = box.localToGlobal(Offset.zero);
    await tester.drag(find.byKey(contentKey), const Offset(0.0, -20.0));

    expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -20.0)));

    // Try dragging by the additional children in contents.
    box = tester.renderObject(find.byKey(contentKey));
    originalOffset = box.localToGlobal(Offset.zero);
    await tester.drag(find.byKey(contentKey), const Offset(0.0, -20.0));

    expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -20.0)));
  });

  testWidgetsWithLeakTracking("LicensePage's color must be same whether loading or done", (WidgetTester tester) async {
    const Color scaffoldColor = Color(0xFF123456);
    const Color cardColor = Color(0xFF654321);

    await tester.pumpWidget(MaterialApp(
      theme: ThemeData.light(useMaterial3: false).copyWith(
        scaffoldBackgroundColor: scaffoldColor,
        cardColor: cardColor,
      ),
      home: Scaffold(
        body: Center(
          child: Builder(
            builder: (BuildContext context) => GestureDetector(
              child: const Text('Show licenses'),
              onTap: () {
                showLicensePage(
                  context: context,
                  applicationName: 'MyApp',
                  applicationVersion: '1.0.0',
                );
              },
            ),
          ),
        ),
      ),
    ));

    await tester.tap(find.text('Show licenses'));
    await tester.pump();
    await tester.pump();

    // Check color when loading.
    final List<Material> materialLoadings = tester.widgetList<Material>(find.byType(Material)).toList();
    expect(materialLoadings.length, equals(4));
    expect(materialLoadings[1].color, scaffoldColor);
    expect(materialLoadings[2].color, cardColor);

    await tester.pumpAndSettle();

    // Check color when done.
    expect(find.byKey(const ValueKey<ConnectionState>(ConnectionState.done)), findsOneWidget);
    final List<Material> materialDones = tester.widgetList<Material>(find.byType(Material)).toList();
    expect(materialDones.length, equals(3));
    expect(materialDones[0].color, scaffoldColor);
    expect(materialDones[1].color, cardColor);
  });

  testWidgetsWithLeakTracking('Conflicting scrollbars are not applied by ScrollBehavior to _PackageLicensePage', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/83819
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['AAA'], 'BBB'),
      ]);
    });

    await tester.pumpWidget(
      const MaterialApp(
        home: Center(
          child: LicensePage(),
        ),
      ),
    );
    await tester.pumpAndSettle();

    // Check for packages.
    expect(find.text('AAA'), findsOneWidget);
    // Check license is displayed after entering into license page for 'AAA'.
    await tester.tap(find.text('AAA'));
    await tester.pumpAndSettle();

    // The inherited ScrollBehavior should not apply Scrollbars since they are
    // already built in to the widget.
    switch (debugDefaultTargetPlatformOverride) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        expect(find.byType(CupertinoScrollbar), findsNothing);
      case TargetPlatform.iOS:
        expect(find.byType(CupertinoScrollbar), findsOneWidget);
      case null:
        break;
    }
    expect(find.byType(Scrollbar), findsOneWidget);
    expect(find.byType(RawScrollbar), findsNothing);

  }, variant: TargetPlatformVariant.all());

  testWidgetsWithLeakTracking('ListView of license entries is primary', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/120710
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
         LicenseEntryWithLineBreaks(
          <String>['AAA'],
          // Add enough content to scroll
          List<String>.generate(500, (int index) => 'BBBB').join('\n'),
        ),
      ]);
    });

    await tester.pumpWidget(
      MaterialApp(
        title: 'Flutter Code Sample',
        home: Scaffold(
          body: Builder(
            builder: (BuildContext context) => TextButton(
              child: const Text('Show License Page'),
              onPressed: () {
                showLicensePage(context: context);
              },
            ),
          ),
        ),
      )
    );
    await tester.pumpAndSettle();

    expect(find.text('Show License Page'), findsOneWidget);
    await tester.tap(find.text('Show License Page'));
    await tester.pumpAndSettle();

    // Check for packages.
    expect(find.text('AAA'), findsOneWidget);
    // Check license is displayed after entering into license page for 'AAA'.
    await tester.tap(find.text('AAA'));
    await tester.pumpAndSettle();

    // The inherited ScrollBehavior should not apply Scrollbars since they are
    // already built in to the widget.
    switch (debugDefaultTargetPlatformOverride) {
      case TargetPlatform.android:
      case TargetPlatform.fuchsia:
      case TargetPlatform.linux:
      case TargetPlatform.macOS:
      case TargetPlatform.windows:
        expect(find.byType(CupertinoScrollbar), findsNothing);
      case TargetPlatform.iOS:
        expect(find.byType(CupertinoScrollbar), findsOneWidget);
      case null:
        break;
    }
    expect(find.byType(Scrollbar), findsOneWidget);
    expect(find.byType(RawScrollbar), findsNothing);
    await tester.drag(find.byType(ListView), const Offset(0.0, 20.0));
    await tester.pumpAndSettle(); // No exception triggered.
  }, variant: TargetPlatformVariant.all());

  testWidgetsWithLeakTracking('LicensePage padding', (WidgetTester tester) async {
    const FlutterLogo logo = FlutterLogo();

    await tester.pumpWidget(
      const MaterialApp(
        title: 'Pirate app',
        home: Center(
          child: LicensePage(
            applicationName: 'LicensePage test app',
            applicationIcon: logo,
            applicationVersion: '0.1.2',
            applicationLegalese: 'I am the very model of a modern major general.',
          ),
        ),
      ),
    );

    final Finder appName = find.text('LicensePage test app');
    final Finder appIcon = find.byType(FlutterLogo);
    final Finder appVersion = find.text('0.1.2');
    final Finder appLegalese = find.text('I am the very model of a modern major general.');
    final Finder appPowered = find.text('Powered by Flutter');

    expect(appName, findsOneWidget);
    expect(appIcon, findsOneWidget);
    expect(appVersion, findsOneWidget);
    expect(appLegalese, findsOneWidget);
    expect(appPowered, findsOneWidget);

    // Bottom padding is applied to the app version and app legalese text.
    final double appNameBottomPadding = tester.getTopLeft(appIcon).dy - tester.getBottomLeft(appName).dy;
    expect(appNameBottomPadding, 0.0);

    final double appIconBottomPadding = tester.getTopLeft(appVersion).dy - tester.getBottomLeft(appIcon).dy;
    expect(appIconBottomPadding, 0.0);

    final double appVersionBottomPadding = tester.getTopLeft(appLegalese).dy - tester.getBottomLeft(appVersion).dy;
    expect(appVersionBottomPadding, 18.0);

    final double appLegaleseBottomPadding = tester.getTopLeft(appPowered).dy - tester.getBottomLeft(appLegalese).dy;
    expect(appLegaleseBottomPadding, 18.0);
  });

  testWidgetsWithLeakTracking('LicensePage has no extra padding between app icon and app powered text', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/99559

    const FlutterLogo logo = FlutterLogo();

    await tester.pumpWidget(
      const MaterialApp(
        title: 'Pirate app',
        home: Center(
          child: LicensePage(
            applicationIcon: logo,
          ),
        ),
      ),
    );

    final Finder appName = find.text('LicensePage test app');
    final Finder appIcon = find.byType(FlutterLogo);
    final Finder appVersion = find.text('0.1.2');
    final Finder appLegalese = find.text('I am the very model of a modern major general.');
    final Finder appPowered = find.text('Powered by Flutter');

    expect(appName, findsNothing);
    expect(appIcon, findsOneWidget);
    expect(appVersion, findsNothing);
    expect(appLegalese, findsNothing);
    expect(appPowered, findsOneWidget);

    // Padding between app icon and app powered text.
    final double appIconBottomPadding = tester.getTopLeft(appPowered).dy - tester.getBottomLeft(appIcon).dy;
    expect(appIconBottomPadding, 18.0);
  });

  testWidgetsWithLeakTracking('Error handling test', (WidgetTester tester) async {
    LicenseRegistry.addLicense(() => Stream<LicenseEntry>.error(Exception('Injected failure')));
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: const Material(child: AboutListTile())
      )
    );
    await tester.tap(find.byType(ListTile));
    await tester.pump();
    await tester.pump(const Duration(seconds: 2));
    await tester.tap(find.text('VIEW LICENSES'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 2));
    final Finder finder = find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_PackagesView');
    // force the stream to complete (has to be done in a runAsync block since it's areal async process)
    await tester.runAsync(() => (tester.firstState(finder) as dynamic).licenses as Future<dynamic>); // ignore: avoid_dynamic_calls
    expect(tester.takeException().toString(), 'Exception: Injected failure');
    await tester.pumpAndSettle();
    expect(tester.takeException().toString(), 'Exception: Injected failure');
    expect(find.text('Exception: Injected failure'), findsOneWidget);
  });

  testWidgetsWithLeakTracking('LicensePage master view layout position - ltr', (WidgetTester tester) async {
    const TextDirection textDirection = TextDirection.ltr;
    const Size defaultSize = Size(800.0, 600.0);
    const Size wideSize = Size(1200.0, 600.0);
    const String title = 'License ABC';
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'),
      ]);
    });

    // Configure to show the default layout.
    await tester.binding.setSurfaceSize(defaultSize);

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        title: title,
        home: const Scaffold(
          body: Directionality(
            textDirection: textDirection,
            child: LicensePage(),
          ),
        ),
      ),
    );

    await tester.pumpAndSettle(); // Finish rendering the page.

    // If the layout width is less than 840.0 pixels, nested layout is
    // used which positions license page title at the top center.
    Offset titleOffset = tester.getCenter(find.text(title));
    expect(titleOffset, Offset(defaultSize.width / 2, 92.0));
    expect(tester.getCenter(find.byType(ListView)), Offset(defaultSize.width / 2, 328.0));

    // Configure a wide window to show the lateral UI.
    await tester.binding.setSurfaceSize(wideSize);

    await tester.pumpWidget(
      const MaterialApp(
        title: title,
        home: Scaffold(
          body: Directionality(
            textDirection: textDirection,
            child: LicensePage(),
          ),
        ),
      ),
    );

    await tester.pumpAndSettle(); // Finish rendering the page.

    // If the layout width is greater than 840.0 pixels, lateral UI layout
    // is used which positions license page title and packageList
    // at the top left.
    titleOffset = tester.getTopRight(find.text(title));
    expect(titleOffset, const Offset(292.0, 136.0));
    expect(titleOffset.dx, lessThan(wideSize.width - 320)); // Default master view width is 320.0.
    expect(tester.getCenter(find.byType(ListView)), const Offset(160, 356));

    // Configure to show the default layout.
    await tester.binding.setSurfaceSize(defaultSize);
  });

  testWidgetsWithLeakTracking('LicensePage master view layout position - rtl', (WidgetTester tester) async {
    const TextDirection textDirection = TextDirection.rtl;
    const Size defaultSize = Size(800.0, 600.0);
    const Size wideSize = Size(1200.0, 600.0);
    const String title = 'License ABC';
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'),
      ]);
    });

    // Configure to show the default layout.
    await tester.binding.setSurfaceSize(defaultSize);

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        title: title,
        home: const Scaffold(
          body: Directionality(
            textDirection: textDirection,
            child: LicensePage(),
          ),
        ),
      ),
    );

    await tester.pumpAndSettle(); // Finish rendering the page.

    // If the layout width is less than 840.0 pixels, nested layout is
    // used which positions license page title at the top center.
    Offset titleOffset = tester.getCenter(find.text(title));
    expect(titleOffset, Offset(defaultSize.width / 2, 92.0));
    expect(tester.getCenter(find.byType(ListView)), Offset(defaultSize.width / 2, 328.0));

    // Configure a wide window to show the lateral UI.
    await tester.binding.setSurfaceSize(wideSize);

    await tester.pumpWidget(
      const MaterialApp(
        title: title,
        home: Scaffold(
          body: Directionality(
            textDirection: textDirection,
            child: LicensePage(),
          ),
        ),
      ),
    );

    await tester.pumpAndSettle(); // Finish rendering the page.

    // If the layout width is greater than 840.0 pixels, lateral UI layout
    // is used which positions license page title and packageList
    // at the top right.
    titleOffset = tester.getTopLeft(find.text(title));
    expect(titleOffset, const Offset(908.0, 136.0));
    expect(titleOffset.dx, greaterThan(wideSize.width - 320)); // Default master view width is 320.0.
    expect(tester.getCenter(find.byType(ListView)), const Offset(1040.0, 356.0));

    // Configure to show the default layout.
    await tester.binding.setSurfaceSize(defaultSize);
  });

  testWidgetsWithLeakTracking('License page title in lateral UI does not use AppBarTheme.foregroundColor', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/108991
    final ThemeData theme = ThemeData(
      appBarTheme: const AppBarTheme(foregroundColor: Color(0xFFFFFFFF)),
      useMaterial3: true,
    );
    const String title = 'License ABC';
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'),
      ]);
    });

    // Configure a wide window to show the lateral UI.
    await tester.binding.setSurfaceSize(const Size(1200.0, 600.0));

    await tester.pumpWidget(
      MaterialApp(
        title: title,
        theme: theme,
        home: const Scaffold(
          body: LicensePage(),
        ),
      ),
    );

    await tester.pumpAndSettle(); // Finish rendering the page.

    final RenderParagraph renderParagraph = tester.renderObject(find.text('ABC').last) as RenderParagraph;

    // License page title should not use AppBarTheme's foregroundColor.
    expect(renderParagraph.text.style!.color, isNot(theme.appBarTheme.foregroundColor));

    // License page title in the lateral UI uses default text style color.
    expect(renderParagraph.text.style!.color, theme.textTheme.titleLarge!.color);

    // Configure to show the default layout.
    await tester.binding.setSurfaceSize(const Size(800.0, 600.0));
  });

  testWidgetsWithLeakTracking('License page default title text color in the nested UI', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/108991
    final ThemeData theme = ThemeData(useMaterial3: true);
    const String title = 'License ABC';
    LicenseRegistry.addLicense(() {
      return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
        const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'),
      ]);
    });

    await tester.pumpWidget(
      MaterialApp(
        title: title,
        theme: theme,
        home: const Scaffold(
          body: LicensePage(),
        ),
      ),
    );

    await tester.pumpAndSettle(); // Finish rendering the page.

    // Currently in the master view.
    expect(find.text('License ABC'), findsOneWidget);

    // Navigate to the license page.
    await tester.tap(find.text('ABC'));
    await tester.pumpAndSettle();

    // Master view is no longer visible.
    expect(find.text('License ABC'), findsNothing);

    final RenderParagraph renderParagraph = tester.renderObject(find.text('ABC').first) as RenderParagraph;
    expect(renderParagraph.text.style!.color, theme.textTheme.titleLarge!.color);
  });

  group('Material 2', () {
    // These tests are only relevant for Material 2. Once Material 2
    // support is deprecated and the APIs are removed, these tests
    // can be deleted.

    testWidgetsWithLeakTracking('License page default title text color in the nested UI', (WidgetTester tester) async {
      // This is a regression test for https://github.com/flutter/flutter/issues/108991
      final ThemeData theme = ThemeData(useMaterial3: false);
      const String title = 'License ABC';
      LicenseRegistry.addLicense(() {
        return Stream<LicenseEntry>.fromIterable(<LicenseEntry>[
          const LicenseEntryWithLineBreaks(<String>['ABC'], 'DEF'),
        ]);
      });

      await tester.pumpWidget(
        MaterialApp(
          title: title,
          theme: theme,
          home: const Scaffold(
            body: LicensePage(),
          ),
        ),
      );

      await tester.pumpAndSettle(); // Finish rendering the page.

      // Currently in the master view.
      expect(find.text('License ABC'), findsOneWidget);

      // Navigate to the license page.
      await tester.tap(find.text('ABC'));
      await tester.pumpAndSettle();

      // Master view is no longer visible.
      expect(find.text('License ABC'), findsNothing);

      final RenderParagraph renderParagraph = tester.renderObject(find.text('ABC').first) as RenderParagraph;
      expect(renderParagraph.text.style!.color, theme.primaryTextTheme.titleLarge!.color);
    });
  });
}

class FakeLicenseEntry extends LicenseEntry {
  FakeLicenseEntry();

  bool get packagesCalled => _packagesCalled;
  bool _packagesCalled = false;

  @override
  Iterable<LicenseParagraph> paragraphs = <LicenseParagraph>[];

  @override
  Iterable<String> get packages {
    _packagesCalled = true;
    return <String>[];
  }
}

class LicensePageObserver extends NavigatorObserver {
  int licensePageCount = 0;

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route is MaterialPageRoute<dynamic>) {
      licensePageCount++;
    }
    super.didPush(route, previousRoute);
  }
}

class AboutDialogObserver extends NavigatorObserver {
  int dialogCount = 0;

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