// 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:ui';

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

import '../widgets/semantics_tester.dart';

MaterialApp _buildAppWithDialog(Widget dialog, { ThemeData? theme, double textScaleFactor = 1.0 }) {
  return MaterialApp(
    theme: theme,
    home: Material(
      child: Builder(
        builder: (BuildContext context) {
          return Center(
            child: ElevatedButton(
              child: const Text('X'),
              onPressed: () {
                showDialog<void>(
                  context: context,
                  builder: (BuildContext context) {
                    return MediaQuery(
                      data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor),
                      child: dialog,
                    );
                  },
                );
              },
            ),
          );
        }
      ),
    ),
  );
}

Material _getMaterialFromDialog(WidgetTester tester) {
  return tester.widget<Material>(find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)));
}

RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) {
  return tester.element<StatelessElement>(find.descendant(of: find.byType(AlertDialog), matching: find.text(text))).renderObject! as RenderParagraph;
}

const ShapeBorder _defaultDialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));

void main() {
  testWidgets('Dialog is scrollable', (WidgetTester tester) async {
    bool didPressOk = false;
    final AlertDialog dialog = AlertDialog(
      content: Container(
        height: 5000.0,
        width: 300.0,
        color: Colors.green[500],
      ),
      actions: <Widget>[
        TextButton(
            onPressed: () {
              didPressOk = true;
            },
            child: const Text('OK'),
        ),
      ],
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    expect(didPressOk, false);
    await tester.tap(find.text('OK'));
    expect(didPressOk, true);
  });

  testWidgets('Dialog background color from AlertDialog', (WidgetTester tester) async {
    const Color customColor = Colors.pink;
    const AlertDialog dialog = AlertDialog(
      backgroundColor: customColor,
      actions: <Widget>[ ],
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(brightness: Brightness.dark)));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Material materialWidget = _getMaterialFromDialog(tester);
    expect(materialWidget.color, customColor);
  });

  testWidgets('Dialog Defaults', (WidgetTester tester) async {
    const AlertDialog dialog = AlertDialog(
      title: Text('Title'),
      content: Text('Y'),
      actions: <Widget>[ ],
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(brightness: Brightness.dark)));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Material materialWidget = _getMaterialFromDialog(tester);
    expect(materialWidget.color, Colors.grey[800]);
    expect(materialWidget.shape, _defaultDialogShape);
    expect(materialWidget.elevation, 24.0);
  });

  testWidgets('Custom dialog elevation', (WidgetTester tester) async {
    const double customElevation = 12.0;
    const AlertDialog dialog = AlertDialog(
      actions: <Widget>[ ],
      elevation: customElevation,
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Material materialWidget = _getMaterialFromDialog(tester);
    expect(materialWidget.elevation, customElevation);
  });

  testWidgets('Custom Title Text Style', (WidgetTester tester) async {
    const String titleText = 'Title';
    const TextStyle titleTextStyle = TextStyle(color: Colors.pink);
    const AlertDialog dialog = AlertDialog(
      title: Text(titleText),
      titleTextStyle: titleTextStyle,
      actions: <Widget>[ ],
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final RenderParagraph title = _getTextRenderObjectFromDialog(tester, titleText);
    expect(title.text.style, titleTextStyle);
  });

  testWidgets('Custom Content Text Style', (WidgetTester tester) async {
    const String contentText = 'Content';
    const TextStyle contentTextStyle = TextStyle(color: Colors.pink);
    const AlertDialog dialog = AlertDialog(
      content: Text(contentText),
      contentTextStyle: contentTextStyle,
      actions: <Widget>[ ],
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
    expect(content.text.style, contentTextStyle);
  });

  testWidgets('Custom clipBehavior', (WidgetTester tester) async {
    const AlertDialog dialog = AlertDialog(
      actions: <Widget>[],
      clipBehavior: Clip.antiAlias,
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Material materialWidget = _getMaterialFromDialog(tester);
    expect(materialWidget.clipBehavior, Clip.antiAlias);
  });

  testWidgets('Custom dialog shape', (WidgetTester tester) async {
    const RoundedRectangleBorder customBorder =
      RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0)));
    const AlertDialog dialog = AlertDialog(
      actions: <Widget>[ ],
      shape: customBorder,
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Material materialWidget = _getMaterialFromDialog(tester);
    expect(materialWidget.shape, customBorder);
  });

  testWidgets('Null dialog shape', (WidgetTester tester) async {
    const AlertDialog dialog = AlertDialog(
      actions: <Widget>[ ],
      shape: null,
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Material materialWidget = _getMaterialFromDialog(tester);
    expect(materialWidget.shape, _defaultDialogShape);
  });

  testWidgets('Rectangular dialog shape', (WidgetTester tester) async {
    const ShapeBorder customBorder = Border();
    const AlertDialog dialog = AlertDialog(
      actions: <Widget>[ ],
      shape: customBorder,
    );
    await tester.pumpWidget(_buildAppWithDialog(dialog));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Material materialWidget = _getMaterialFromDialog(tester);
    expect(materialWidget.shape, customBorder);
  });

  testWidgets('showDialog child and builder cannot be simultaneously defined', (WidgetTester tester) async {
    late BuildContext currentBuildContext;
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: Builder(
              builder: (BuildContext context) {
                currentBuildContext = context;
                return Container();
              }
            ),
          ),
        ),
      ),
    );

    expect(() async {
      showDialog<void>(
        context: currentBuildContext,
        child: const Text('Child'),
        builder: (BuildContext context) {
          return const Text('Builder');
        },
      );
    }, throwsAssertionError);
  });

  testWidgets('showDialog child or builder must be defined', (WidgetTester tester) async {
    late BuildContext currentBuildContext;
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: Builder(
              builder: (BuildContext context) {
                currentBuildContext = context;
                return Container();
              }
            ),
          ),
        ),
      ),
    );

    expect(
      () => showDialog<void>(context: currentBuildContext),
      throwsAssertionError,
    );
  });

  testWidgets('Simple dialog control test', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Center(
            child: ElevatedButton(
              onPressed: null,
              child: Text('Go'),
            ),
          ),
        ),
      ),
    );

    final BuildContext context = tester.element(find.text('Go'));

    final Future<int?> result = showDialog<int>(
      context: context,
      builder: (BuildContext context) {
        return SimpleDialog(
          title: const Text('Title'),
          children: <Widget>[
            SimpleDialogOption(
              onPressed: () {
                Navigator.pop(context, 42);
              },
              child: const Text('First option'),
            ),
            const SimpleDialogOption(
              child: Text('Second option'),
            ),
          ],
        );
      },
    );

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(find.text('Title'), findsOneWidget);
    await tester.tap(find.text('First option'));

    expect(await result, equals(42));
  });

  testWidgets('Can show dialog using navigator global key', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: navigator,
        home: const Material(
          child: Center(
            child: Text('Go'),
          ),
        ),
      ),
    );

    final Future<int?> result = showDialog<int>(
      context: navigator.currentContext!,
      builder: (BuildContext context) {
        return SimpleDialog(
          title: const Text('Title'),
          children: <Widget>[
            SimpleDialogOption(
              onPressed: () {
                Navigator.pop(context, 42);
              },
              child: const Text('First option'),
            ),
            const SimpleDialogOption(
              child: Text('Second option'),
            ),
          ],
        );
      },
    );

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(find.text('Title'), findsOneWidget);
    await tester.tap(find.text('First option'));

    expect(await result, equals(42));
  });

  testWidgets('Custom padding on SimpleDialogOption', (WidgetTester tester) async {
    const EdgeInsets customPadding = EdgeInsets.fromLTRB(4, 10, 8, 6);
    final SimpleDialog dialog = SimpleDialog(
      title: const Text('Title'),
      children: <Widget>[
        SimpleDialogOption(
          onPressed: () {},
          child: const Text('First option'),
          padding: customPadding,
        ),
      ],
    );

    await tester.pumpWidget(_buildAppWithDialog(dialog));
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Rect dialogRect = tester.getRect(find.byType(SimpleDialogOption));
    final Rect textRect = tester.getRect(find.text('First option'));

    expect(textRect.left, dialogRect.left + customPadding.left);
    expect(textRect.top, dialogRect.top + customPadding.top);
    expect(textRect.right, dialogRect.right - customPadding.right);
    expect(textRect.bottom, dialogRect.bottom - customPadding.bottom);
  });

  testWidgets('Barrier dismissible', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Center(
            child: ElevatedButton(
              onPressed: null,
              child: Text('Go'),
            ),
          ),
        ),
      ),
    );

    final BuildContext context = tester.element(find.text('Go'));

    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return Container(
          width: 100.0,
          height: 100.0,
          alignment: Alignment.center,
          child: const Text('Dialog1'),
        );
      },
    );

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(find.text('Dialog1'), findsOneWidget);

    // Tap on the barrier.
    await tester.tapAt(const Offset(10.0, 10.0));

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(find.text('Dialog1'), findsNothing);

    showDialog<void>(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return Container(
          width: 100.0,
          height: 100.0,
          alignment: Alignment.center,
          child: const Text('Dialog2'),
        );
      },
    );

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(find.text('Dialog2'), findsOneWidget);

    // Tap on the barrier, which shouldn't do anything this time.
    await tester.tapAt(const Offset(10.0, 10.0));

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(find.text('Dialog2'), findsOneWidget);

  });

  testWidgets('Barrier color', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Center(child: Text('Test')),
      ),
    );
    final BuildContext context = tester.element(find.text('Test'));

    // Test default barrier color
    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return const Text('Dialog');
      },
    );
    await tester.pumpAndSettle();
    expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.black54);

    // Dismiss it and test a custom barrier color
    await tester.tapAt(const Offset(10.0, 10.0));
    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return const Text('Dialog');
      },
      barrierColor: Colors.pink,
    );
    await tester.pumpAndSettle();
    expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.pink);
  });

  testWidgets('Dialog hides underlying semantics tree', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    const String buttonText = 'A button covered by dialog overlay';
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Center(
            child: ElevatedButton(
              onPressed: null,
              child: Text(buttonText),
            ),
          ),
        ),
      ),
    );

    expect(semantics, includesNodeWith(label: buttonText));

    final BuildContext context = tester.element(find.text(buttonText));

    const String alertText = 'A button in an overlay alert';
    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return const AlertDialog(title: Text(alertText));
      },
    );

    await tester.pumpAndSettle(const Duration(seconds: 1));

    expect(semantics, includesNodeWith(label: alertText));
    expect(semantics, isNot(includesNodeWith(label: buttonText)));

    semantics.dispose();
  });

  testWidgets('AlertDialog.actionsPadding defaults', (WidgetTester tester) async {
    final AlertDialog dialog = AlertDialog(
      title: const Text('title'),
      content: const Text('content'),
      actions: <Widget>[
        ElevatedButton(
          onPressed: () {},
          child: const Text('button'),
        ),
      ],
    );

    await tester.pumpWidget(
      _buildAppWithDialog(dialog),
    );

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    // The [AlertDialog] is the entire screen, since it also contains the scrim.
    // The first [Material] child of [AlertDialog] is the actual dialog
    // itself.
    final Size dialogSize = tester.getSize(
      find.descendant(
        of: find.byType(AlertDialog),
        matching: find.byType(Material),
      ).first,
    );
    final Size actionsSize = tester.getSize(find.byType(ButtonBar));

    expect(actionsSize.width, dialogSize.width);
  });

  testWidgets('AlertDialog.actionsPadding surrounds actions with padding', (WidgetTester tester) async {
    final AlertDialog dialog = AlertDialog(
      title: const Text('title'),
      content: const Text('content'),
      actions: <Widget>[
        ElevatedButton(
          onPressed: () {},
          child: const Text('button'),
        ),
      ],
      actionsPadding: const EdgeInsets.all(30.0), // custom padding value
    );

    await tester.pumpWidget(
      _buildAppWithDialog(dialog),
    );

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    // The [AlertDialog] is the entire screen, since it also contains the scrim.
    // The first [Material] child of [AlertDialog] is the actual dialog
    // itself.
    final Size dialogSize = tester.getSize(
      find.descendant(
        of: find.byType(AlertDialog),
        matching: find.byType(Material),
      ).first,
    );
    final Size actionsSize = tester.getSize(find.byType(ButtonBar));

    expect(actionsSize.width, dialogSize.width - (30.0 * 2));
  });

  testWidgets('AlertDialog.buttonPadding defaults', (WidgetTester tester) async {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();

    final AlertDialog dialog = AlertDialog(
      title: const Text('title'),
      content: const Text('content'),
      actions: <Widget>[
        ElevatedButton(
          key: key1,
          onPressed: () {},
          child: const Text('button 1'),
        ),
        ElevatedButton(
          key: key2,
          onPressed: () {},
          child: const Text('button 2'),
        ),
      ],
    );

    await tester.pumpWidget(
      _buildAppWithDialog(dialog),
    );

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    // Padding between both buttons
    expect(
      tester.getBottomLeft(find.byKey(key2)).dx,
      tester.getBottomRight(find.byKey(key1)).dx + 8.0,
    );

    // Padding between button and edges of the button bar
    // First button
    expect(
      tester.getTopRight(find.byKey(key1)).dy,
      tester.getTopRight(find.byType(ButtonBar)).dy + 8.0,
    ); // top
    expect(
      tester.getBottomRight(find.byKey(key1)).dy,
      tester.getBottomRight(find.byType(ButtonBar)).dy - 8.0,
    ); // bottom

    // Second button
    expect(
      tester.getTopRight(find.byKey(key2)).dy,
      tester.getTopRight(find.byType(ButtonBar)).dy + 8.0,
    ); // top
    expect(
      tester.getBottomRight(find.byKey(key2)).dy,
      tester.getBottomRight(find.byType(ButtonBar)).dy - 8.0,
    ); // bottom
    expect(
      tester.getBottomRight(find.byKey(key2)).dx,
      tester.getBottomRight(find.byType(ButtonBar)).dx - 8.0,
    ); // right
  });

  testWidgets('AlertDialog.buttonPadding custom values', (WidgetTester tester) async {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();

    final AlertDialog dialog = AlertDialog(
      title: const Text('title'),
      content: const Text('content'),
      actions: <Widget>[
        ElevatedButton(
          key: key1,
          onPressed: () {},
          child: const Text('button 1'),
        ),
        ElevatedButton(
          key: key2,
          onPressed: () {},
          child: const Text('button 2'),
        ),
      ],
      buttonPadding: const EdgeInsets.only(
        left: 10.0,
        right: 20.0,
      ),
    );

    await tester.pumpWidget(
      _buildAppWithDialog(dialog),
    );

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    // Padding between both buttons
    expect(
      tester.getBottomLeft(find.byKey(key2)).dx,
      tester.getBottomRight(find.byKey(key1)).dx + ((10.0 + 20.0) / 2),
    );

    // Padding between button and edges of the button bar
    // First button
    expect(
      tester.getTopRight(find.byKey(key1)).dy,
      tester.getTopRight(find.byType(ButtonBar)).dy + ((10.0 + 20.0) / 2),
    ); // top
    expect(
      tester.getBottomRight(find.byKey(key1)).dy,
      tester.getBottomRight(find.byType(ButtonBar)).dy - ((10.0 + 20.0) / 2),
    ); // bottom

    // Second button
    expect(
      tester.getTopRight(find.byKey(key2)).dy,
      tester.getTopRight(find.byType(ButtonBar)).dy + ((10.0 + 20.0) / 2),
    ); // top
    expect(
      tester.getBottomRight(find.byKey(key2)).dy,
      tester.getBottomRight(find.byType(ButtonBar)).dy - ((10.0 + 20.0) / 2),
    ); // bottom
    expect(
      tester.getBottomRight(find.byKey(key2)).dx,
      tester.getBottomRight(find.byType(ButtonBar)).dx - ((10.0 + 20.0) / 2),
    ); // right
  });

  group('Dialog children padding is correct', () {
    final List<double> textScaleFactors = <double>[0.5, 1.0, 1.5, 2.0, 3.0];
    final Map<double, double> paddingScaleFactors = <double, double>{
      0.5: 1.0,
      1.0: 1.0,
      1.5: 2.0 / 3.0,
      2.0: 1.0 / 3.0,
      3.0: 1.0 / 3.0,
    };

    final GlobalKey titleKey = GlobalKey();
    final GlobalKey contentKey = GlobalKey();
    final GlobalKey childrenKey = GlobalKey();

    final Finder dialogFinder = find.descendant(of: find.byType(Dialog), matching: find.byType(Material)).first;
    final Finder titleFinder = find.byKey(titleKey);
    final Finder contentFinder = find.byKey(contentKey);
    final Finder actionsFinder = find.byType(ButtonBar);
    final Finder childrenFinder = find.byKey(childrenKey);

    Future<void> openDialog(WidgetTester tester, Widget dialog, double textScaleFactor) async {
      await tester.pumpWidget(
        _buildAppWithDialog(dialog, textScaleFactor: textScaleFactor),
      );

      await tester.tap(find.text('X'));
      await tester.pumpAndSettle();
    }

    void expectLeftEdgePadding(
      WidgetTester tester, {
      required Finder finder,
      required double textScaleFactor,
      required double unscaledValue,
    }) {
      expect(
        tester.getTopLeft(dialogFinder).dx,
        moreOrLessEquals(tester.getTopLeft(finder).dx - unscaledValue * paddingScaleFactors[textScaleFactor]!),
      );
      expect(
        tester.getBottomLeft(dialogFinder).dx,
        moreOrLessEquals(tester.getBottomLeft(finder).dx - unscaledValue * paddingScaleFactors[textScaleFactor]!),
      );
    }

    void expectRightEdgePadding(
      WidgetTester tester, {
      required Finder finder,
      required double textScaleFactor,
      required double unscaledValue,
    }) {
      expect(
        tester.getTopRight(dialogFinder).dx,
        moreOrLessEquals(tester.getTopRight(finder).dx + unscaledValue * paddingScaleFactors[textScaleFactor]!),
      );
      expect(
        tester.getBottomRight(dialogFinder).dx,
        moreOrLessEquals(tester.getBottomRight(finder).dx + unscaledValue * paddingScaleFactors[textScaleFactor]!),
      );
    }

    void expectTopEdgePadding(
      WidgetTester tester, {
      required Finder finder,
      required double textScaleFactor,
      required double unscaledValue,
    }) {
      expect(
        tester.getTopLeft(dialogFinder).dy,
        moreOrLessEquals(tester.getTopLeft(finder).dy - unscaledValue * paddingScaleFactors[textScaleFactor]!),
      );
      expect(
        tester.getTopRight(dialogFinder).dy,
        moreOrLessEquals(tester.getTopRight(finder).dy - unscaledValue * paddingScaleFactors[textScaleFactor]!),
      );
    }

    void expectBottomEdgePadding(
      WidgetTester tester, {
      required Finder finder,
      required double textScaleFactor,
      required double unscaledValue,
    }) {
      expect(
        tester.getBottomLeft(dialogFinder).dy,
        moreOrLessEquals(tester.getBottomRight(finder).dy + unscaledValue * paddingScaleFactors[textScaleFactor]!),
      );
      expect(
        tester.getBottomRight(dialogFinder).dy,
        moreOrLessEquals(tester.getBottomRight(finder).dy + unscaledValue * paddingScaleFactors[textScaleFactor]!),
      );
    }

    void expectVerticalInnerPadding(
    WidgetTester tester, {
      required Finder top,
      required Finder bottom,
      required double value,
    }) {
      expect(
        tester.getBottomLeft(top).dy,
        tester.getTopLeft(bottom).dy - value,
      );
      expect(
        tester.getBottomRight(top).dy,
        tester.getTopRight(bottom).dy - value,
      );
    }

    final Widget title = Text(
      'title',
      key: titleKey,
    );
    final Widget content = Text(
      'content',
      key: contentKey,
    );
    final List<Widget> actions = <Widget>[
      ElevatedButton(
        onPressed: () {},
        child: const Text('button'),
      ),
    ];
    final List<Widget> children = <Widget>[
      SimpleDialogOption(
        key: childrenKey,
        child: const Text('child'),
        onPressed: () { },
      ),
    ];

    for (final double textScaleFactor in textScaleFactors) {
      testWidgets('AlertDialog padding is correct when only title and actions are specified [textScaleFactor]=$textScaleFactor}', (WidgetTester tester) async {
        final AlertDialog dialog = AlertDialog(
          title: title,
          actions: actions,
        );

        await openDialog(tester, dialog, textScaleFactor);

        expectTopEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectRightEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectVerticalInnerPadding(
          tester,
          top: titleFinder,
          bottom: actionsFinder,
          value: 20.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectRightEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectBottomEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
      });

      testWidgets('AlertDialog padding is correct when only content and actions are specified [textScaleFactor]=$textScaleFactor}', (WidgetTester tester) async {
        final AlertDialog dialog = AlertDialog(
          content: content,
          actions: actions,
        );

        await openDialog(tester, dialog, textScaleFactor);

        expectTopEdgePadding(
          tester,
          finder: contentFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 20.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: contentFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectRightEdgePadding(
          tester,
          finder: contentFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectVerticalInnerPadding(
          tester,
          top: contentFinder,
          bottom: actionsFinder,
          value: 24.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectRightEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectBottomEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
      });

      testWidgets('AlertDialog padding is correct when title, content, and actions are specified [textScaleFactor]=$textScaleFactor}', (WidgetTester tester) async {
        final AlertDialog dialog = AlertDialog(
          title: title,
          content: content,
          actions: actions,
        );

        await openDialog(tester, dialog, textScaleFactor);

        expectTopEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectRightEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectVerticalInnerPadding(
          tester,
          top: titleFinder,
          bottom: contentFinder,
          value: 20.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: contentFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectRightEdgePadding(
          tester,
          finder: contentFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectVerticalInnerPadding(
          tester,
          top: contentFinder,
          bottom: actionsFinder,
          value: 24.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectRightEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectBottomEdgePadding(
          tester,
          finder: actionsFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
      });

      testWidgets('SimpleDialog padding is correct when only children are specified [textScaleFactor]=$textScaleFactor}', (WidgetTester tester) async {
        final SimpleDialog dialog = SimpleDialog(
          children: children,
        );

        await openDialog(tester, dialog, textScaleFactor);

        expectTopEdgePadding(
          tester,
          finder: childrenFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 12.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: childrenFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectRightEdgePadding(
          tester,
          finder: childrenFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectBottomEdgePadding(
          tester,
          finder: childrenFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 16.0,
        );
      });

      testWidgets('SimpleDialog padding is correct when title and children are specified [textScaleFactor]=$textScaleFactor}', (WidgetTester tester) async {
        final SimpleDialog dialog = SimpleDialog(
          title: title,
          children: children,
        );

        await openDialog(tester, dialog, textScaleFactor);

        expectTopEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectRightEdgePadding(
          tester,
          finder: titleFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 24.0,
        );
        expectVerticalInnerPadding(
          tester,
          top: titleFinder,
          bottom: childrenFinder,
          value: 12.0,
        );
        expectLeftEdgePadding(
          tester,
          finder: childrenFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectRightEdgePadding(
          tester,
          finder: childrenFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 0.0,
        );
        expectBottomEdgePadding(
          tester,
          finder: childrenFinder,
          textScaleFactor: textScaleFactor,
          unscaledValue: 16.0,
        );
      });
    }
  });

  testWidgets('Dialogs can set the vertical direction of overflowing actions', (WidgetTester tester) async {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();

    final AlertDialog dialog = AlertDialog(
      title: const Text('title'),
      content: const Text('content'),
      actions: <Widget>[
        ElevatedButton(
          key: key1,
          onPressed: () {},
          child: const Text('Looooooooooooooong button 1'),
        ),
        ElevatedButton(
          key: key2,
          onPressed: () {},
          child: const Text('Looooooooooooooong button 2'),
        ),
      ],
      actionsOverflowDirection: VerticalDirection.up,
    );

    await tester.pumpWidget(
      _buildAppWithDialog(dialog),
    );

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Rect buttonOneRect = tester.getRect(find.byKey(key1));
    final Rect buttonTwoRect = tester.getRect(find.byKey(key2));
    // Second [ElevatedButton] should appear above the first.
    expect(buttonTwoRect.bottom, lessThanOrEqualTo(buttonOneRect.top));
  });

  testWidgets('Dialogs have no spacing by default for overflowing actions', (WidgetTester tester) async {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();

    final AlertDialog dialog = AlertDialog(
      title: const Text('title'),
      content: const Text('content'),
      actions: <Widget>[
        ElevatedButton(
          key: key1,
          onPressed: () {},
          child: const Text('Looooooooooooooong button 1'),
        ),
        ElevatedButton(
          key: key2,
          onPressed: () {},
          child: const Text('Looooooooooooooong button 2'),
        ),
      ],
    );

    await tester.pumpWidget(
      _buildAppWithDialog(dialog),
    );

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Rect buttonOneRect = tester.getRect(find.byKey(key1));
    final Rect buttonTwoRect = tester.getRect(find.byKey(key2));
    expect(buttonOneRect.bottom, buttonTwoRect.top);
  });

  testWidgets('Dialogs can set the button spacing of overflowing actions', (WidgetTester tester) async {
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();

    final AlertDialog dialog = AlertDialog(
      title: const Text('title'),
      content: const Text('content'),
      actions: <Widget>[
        ElevatedButton(
          key: key1,
          onPressed: () {},
          child: const Text('Looooooooooooooong button 1'),
        ),
        ElevatedButton(
          key: key2,
          onPressed: () {},
          child: const Text('Looooooooooooooong button 2'),
        ),
      ],
      actionsOverflowButtonSpacing: 10.0,
    );

    await tester.pumpWidget(
      _buildAppWithDialog(dialog),
    );

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    final Rect buttonOneRect = tester.getRect(find.byKey(key1));
    final Rect buttonTwoRect = tester.getRect(find.byKey(key2));
    expect(buttonOneRect.bottom, buttonTwoRect.top - 10.0);
  });

  testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async {
    late BuildContext outerContext;
    late BuildContext routeContext;
    late BuildContext dialogContext;

    await tester.pumpWidget(Localizations(
      locale: const Locale('en', 'US'),
      delegates: const <LocalizationsDelegate<dynamic>>[
        DefaultWidgetsLocalizations.delegate,
        DefaultMaterialLocalizations.delegate,
      ],
      child: MediaQuery(
        data: const MediaQueryData(
          padding: EdgeInsets.all(50.0),
          viewInsets: EdgeInsets.only(left: 25.0, bottom: 75.0),
        ),
        child: Navigator(
          onGenerateRoute: (_) {
            return PageRouteBuilder<void>(
              pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
                outerContext = context;
                return Container();
              },
            );
          },
        ),
      ),
    ));

    showDialog<void>(
      context: outerContext,
      barrierDismissible: false,
      builder: (BuildContext context) {
        routeContext = context;
        return Dialog(
          child: Builder(
            builder: (BuildContext context) {
              dialogContext = context;
              return const Placeholder();
            },
          ),
        );
      },
    );

    await tester.pump();

    expect(MediaQuery.of(outerContext).padding, const EdgeInsets.all(50.0));
    expect(MediaQuery.of(routeContext).padding, EdgeInsets.zero);
    expect(MediaQuery.of(dialogContext).padding, EdgeInsets.zero);
    expect(MediaQuery.of(outerContext).viewInsets, const EdgeInsets.only(left: 25.0, bottom: 75.0));
    expect(MediaQuery.of(routeContext).viewInsets, const EdgeInsets.only(left: 25.0, bottom: 75.0));
    expect(MediaQuery.of(dialogContext).viewInsets, EdgeInsets.zero);
  });

  testWidgets('Dialog widget insets by viewInsets', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MediaQuery(
        data: MediaQueryData(
          viewInsets: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
        ),
        child: Dialog(
          child: Placeholder(),
        ),
      ),
    );
    expect(
      tester.getRect(find.byType(Placeholder)),
      const Rect.fromLTRB(10.0 + 40.0, 20.0 + 24.0, 800.0 - (40.0 + 30.0), 600.0 - (24.0 + 40.0)),
    );
    await tester.pumpWidget(
      const MediaQuery(
        data: MediaQueryData(
          viewInsets: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
        ),
        child: Dialog(
          child: Placeholder(),
        ),
      ),
    );
    expect( // no change because this is an animation
      tester.getRect(find.byType(Placeholder)),
      const Rect.fromLTRB(10.0 + 40.0, 20.0 + 24.0, 800.0 - (40.0 + 30.0), 600.0 - (24.0 + 40.0)),
    );
    await tester.pump(const Duration(seconds: 1));
    expect( // animation finished
      tester.getRect(find.byType(Placeholder)),
      const Rect.fromLTRB(40.0, 24.0, 800.0 - 40.0, 600.0 - 24.0),
    );
  });

  testWidgets('Dialog insetPadding added to outside of dialog', (WidgetTester tester) async {
    // The default testing screen (800, 600)
    const Rect screenRect = Rect.fromLTRB(0.0, 0.0, 800.0, 600.0);

    // Test with no padding
    await tester.pumpWidget(
      const MediaQuery(
        data: MediaQueryData(
          viewInsets: EdgeInsets.all(0.0),
        ),
        child: Dialog(
          child: Placeholder(),
          insetPadding: null,
        ),
      ),
    );
    await tester.pumpAndSettle();
    expect(tester.getRect(find.byType(Placeholder)), screenRect);

    // Test with an insetPadding
    await tester.pumpWidget(
      const MediaQuery(
        data: MediaQueryData(
          viewInsets: EdgeInsets.all(0.0),
        ),
        child: Dialog(
          insetPadding: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
          child: Placeholder(),
        ),
      ),
    );
    await tester.pumpAndSettle();
    expect(tester.getRect(find.byType(Placeholder)),
        Rect.fromLTRB(
          screenRect.left + 10.0,
          screenRect.top + 20.0,
          screenRect.right - 30.0,
          screenRect.bottom - 40.0,
        ));
  });

  testWidgets('AlertDialog widget contains route semantics from title for iOS', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(platform: TargetPlatform.iOS),
        home: Material(
          child: Builder(
            builder: (BuildContext context) {
              return Center(
                child: ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    showDialog<void>(
                      context: context,
                      builder: (BuildContext context) {
                        return const AlertDialog(
                          title: Text('Title'),
                          content: Text('Y'),
                          actions: <Widget>[],
                        );
                      },
                    );
                  },
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(semantics, isNot(includesNodeWith(
        label: 'Title',
        flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
    )));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    expect(semantics, includesNodeWith(
      label: 'Title',
      flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
    ));

    semantics.dispose();
  });

  testWidgets('AlertDialog widget always contains alert route semantics for android', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(platform: TargetPlatform.android),
        home: Material(
          child: Builder(
            builder: (BuildContext context) {
              return Center(
                child: ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    showDialog<void>(
                      context: context,
                      builder: (BuildContext context) {
                        return const AlertDialog(
                          title: Text('Title'),
                          content: Text('Y'),
                          actions: <Widget>[],
                        );
                      },
                    );
                  },
                ),
              );
            },
          ),
        ),
      ),
    );

    expect(semantics, isNot(includesNodeWith(
      label: 'Title',
      flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
    )));
    expect(semantics, isNot(includesNodeWith(
      label: 'Alert',
      flags: <SemanticsFlag>[SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute],
    )));

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    // It does not use 'Title' as route semantics
    expect(semantics, isNot(includesNodeWith(
      label: 'Title',
      flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
    )));
    expect(semantics, includesNodeWith(
      label: 'Alert',
      flags: <SemanticsFlag>[SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute],
    ));

    semantics.dispose();
  });

  testWidgets('SimpleDialog does not introduce additional node', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(platform: TargetPlatform.android),
        home: Material(
          child: Builder(
            builder: (BuildContext context) {
              return Center(
                child: ElevatedButton(
                  child: const Text('X'),
                  onPressed: () {
                    showDialog<void>(
                      context: context,
                      builder: (BuildContext context) {
                        return const SimpleDialog(
                          title: Text('Title'),
                          semanticLabel: 'label',
                        );
                      },
                    );
                  },
                ),
              );
            },
          ),
        ),
      ),
    );

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    // A scope route is not focusable in accessibility service.
    expect(semantics, includesNodeWith(
      label: 'label',
      flags: <SemanticsFlag>[SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute],
    ));

    semantics.dispose();
  });

  testWidgets('Dismissible.confirmDismiss defers to an AlertDialog', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
    final List<int> dismissedItems = <int>[];

    // Dismiss is confirmed IFF confirmDismiss() returns true.
    Future<bool?> confirmDismiss (DismissDirection dismissDirection) async {
      return showDialog<bool>(
        context: _scaffoldKey.currentContext!,
        barrierDismissible: true, // showDialog() returns null if tapped outside the dialog
        builder: (BuildContext context) {
          return AlertDialog(
            actions: <Widget>[
              TextButton(
                child: const Text('TRUE'),
                onPressed: () {
                  Navigator.pop(context, true); // showDialog() returns true
                },
              ),
              TextButton(
                child: const Text('FALSE'),
                onPressed: () {
                  Navigator.pop(context, false); // showDialog() returns false
                },
              ),
            ],
          );
        },
      );
    }

    Widget buildDismissibleItem(int item, StateSetter setState) {
      return Dismissible(
        key: ValueKey<int>(item),
        confirmDismiss: confirmDismiss,
        onDismissed: (DismissDirection direction) {
          setState(() {
            expect(dismissedItems.contains(item), isFalse);
            dismissedItems.add(item);
          });
        },
        child: SizedBox(
          height: 100.0,
          child: Text(item.toString()),
        ),
      );
    }

    Widget buildFrame() {
      return MaterialApp(
        home: StatefulBuilder(
          builder: (BuildContext context, StateSetter setState) {
            return Scaffold(
              key: _scaffoldKey,
              body: Padding(
                padding: const EdgeInsets.all(16.0),
                child: ListView(
                  itemExtent: 100.0,
                  children: <int>[0, 1, 2, 3, 4]
                    .where((int i) => !dismissedItems.contains(i))
                    .map<Widget>((int item) => buildDismissibleItem(item, setState)).toList(),
                ),
              ),
            );
          },
        ),
      );
    }

    Future<void> dismissItem(WidgetTester tester, int item) async {
      await tester.fling(find.text(item.toString()), const Offset(300.0, 0.0), 1000.0); // fling to the right
      await tester.pump(); // start the slide
      await tester.pump(const Duration(seconds: 1)); // finish the slide and start shrinking...
      await tester.pump(); // first frame of shrinking animation
      await tester.pump(const Duration(seconds: 1)); // finish the shrinking and call the callback...
      await tester.pump(); // rebuild after the callback removes the entry
    }

    // Dismiss item 0 is confirmed via the AlertDialog
    await tester.pumpWidget(buildFrame());
    expect(dismissedItems, isEmpty);
    await dismissItem(tester, 0); // Causes the AlertDialog to appear per confirmDismiss
    await tester.pumpAndSettle();
    await tester.tap(find.text('TRUE')); // AlertDialog action
    await tester.pumpAndSettle();
    expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
    expect(find.text('FALSE'), findsNothing);
    expect(dismissedItems, <int>[0]);
    expect(find.text('0'), findsNothing);

    // Dismiss item 1 is not confirmed via the AlertDialog
    await tester.pumpWidget(buildFrame());
    expect(dismissedItems, <int>[0]);
    await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
    await tester.pumpAndSettle();
    await tester.tap(find.text('FALSE')); // AlertDialog action
    await tester.pumpAndSettle();
    expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
    expect(find.text('FALSE'), findsNothing);
    expect(dismissedItems, <int>[0]);
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);

    // Dismiss item 1 is not confirmed via the AlertDialog
    await tester.pumpWidget(buildFrame());
    expect(dismissedItems, <int>[0]);
    await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
    await tester.pumpAndSettle();
    expect(find.text('FALSE'), findsOneWidget);
    expect(find.text('TRUE'), findsOneWidget);
    await tester.tapAt(Offset.zero); // Tap outside of the AlertDialog
    await tester.pumpAndSettle();
    expect(dismissedItems, <int>[0]);
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
    expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
    expect(find.text('FALSE'), findsNothing);

    // Dismiss item 1 is confirmed via the AlertDialog
    await tester.pumpWidget(buildFrame());
    expect(dismissedItems, <int>[0]);
    await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
    await tester.pumpAndSettle();
    await tester.tap(find.text('TRUE')); // AlertDialog action
    await tester.pumpAndSettle();
    expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
    expect(find.text('FALSE'), findsNothing);
    expect(dismissedItems, <int>[0, 1]);
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsNothing);
  });

  // Regression test for https://github.com/flutter/flutter/issues/28505.
  testWidgets('showDialog only gets Theme from context on the first call', (WidgetTester tester) async {
    Widget buildFrame(Key builderKey) {
      return MaterialApp(
        home: Center(
          child: Builder(
            key: builderKey,
            builder: (BuildContext outerContext) {
              return ElevatedButton(
                onPressed: () {
                  showDialog<void>(
                    context: outerContext,
                    builder: (BuildContext innerContext) {
                      return const AlertDialog(title: Text('Title'));
                    },
                  );
                },
                child: const Text('Show Dialog'),
              );
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(UniqueKey()));

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

    // Force the Builder to be recreated (new key) which causes outerContext to
    // be deactivated. If showDialog()'s implementation were to refer to
    // outerContext again, it would crash.
    await tester.pumpWidget(buildFrame(UniqueKey()));
    await tester.pump();
  });

  testWidgets('showDialog safe area', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        builder: (BuildContext context, Widget? child) {
          return MediaQuery(
            // Set up the safe area to be 20 pixels in from each side
            data: const MediaQueryData(padding: EdgeInsets.all(20.0)),
            child: child!,
          );
        },
        home: const Center(child: Text('Test')),
      ),
    );
    final BuildContext context = tester.element(find.text('Test'));

    // By default it should honor the safe area
    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return const Placeholder();
      },
    );
    await tester.pumpAndSettle();
    expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0));
    expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0));

    // Dismiss it and test with useSafeArea off
    await tester.tapAt(const Offset(10.0, 10.0));
    showDialog<void>(
      context: context,
      builder: (BuildContext context) {
        return const Placeholder();
      },
      useSafeArea: false,
    );
    await tester.pumpAndSettle();
    // Should take up the whole screen
    expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(0.0, 0.0));
    expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
  });

  testWidgets('showDialog uses root navigator by default', (WidgetTester tester) async {
    final DialogObserver rootObserver = DialogObserver();
    final DialogObserver nestedObserver = DialogObserver();

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

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

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

  testWidgets('showDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async {
    final DialogObserver rootObserver = DialogObserver();
    final DialogObserver nestedObserver = DialogObserver();

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

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

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

  group('AlertDialog.scrollable: ', () {
    testWidgets('Title is scrollable', (WidgetTester tester) async {
      final Key titleKey = UniqueKey();
      final AlertDialog dialog = AlertDialog(
        title: Container(
          key: titleKey,
          color: Colors.green,
          height: 1000,
        ),
        scrollable: true,
      );
      await tester.pumpWidget(_buildAppWithDialog(dialog));
      await tester.tap(find.text('X'));
      await tester.pumpAndSettle();

      final RenderBox box = tester.renderObject(find.byKey(titleKey));
      final Offset originalOffset = box.localToGlobal(Offset.zero);
      await tester.drag(find.byKey(titleKey), const Offset(0.0, -200.0));
      expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -200.0)));
    });

    testWidgets('Content is scrollable', (WidgetTester tester) async {
      final Key contentKey = UniqueKey();
      final AlertDialog dialog = AlertDialog(
        content: Container(
          key: contentKey,
          color: Colors.orange,
          height: 1000,
        ),
        scrollable: true,
      );
      await tester.pumpWidget(_buildAppWithDialog(dialog));
      await tester.tap(find.text('X'));
      await tester.pumpAndSettle();

      final RenderBox box = tester.renderObject(find.byKey(contentKey));
      final Offset originalOffset = box.localToGlobal(Offset.zero);
      await tester.drag(find.byKey(contentKey), const Offset(0.0, -200.0));
      expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -200.0)));
    });

    testWidgets('Title and content are scrollable', (WidgetTester tester) async {
      final Key titleKey = UniqueKey();
      final Key contentKey = UniqueKey();
      final AlertDialog dialog = AlertDialog(
        title: Container(
          key: titleKey,
          color: Colors.green,
          height: 400,
        ),
        content: Container(
          key: contentKey,
          color: Colors.orange,
          height: 400,
        ),
        scrollable: true,
      );
      await tester.pumpWidget(_buildAppWithDialog(dialog));
      await tester.tap(find.text('X'));
      await tester.pumpAndSettle();

      final RenderBox title = tester.renderObject(find.byKey(titleKey));
      final RenderBox content = tester.renderObject(find.byKey(contentKey));
      final Offset titleOriginalOffset = title.localToGlobal(Offset.zero);
      final Offset contentOriginalOffset = content.localToGlobal(Offset.zero);

      // Dragging the title widget should scroll both the title
      // and the content widgets.
      await tester.drag(find.byKey(titleKey), const Offset(0.0, -200.0));
      expect(title.localToGlobal(Offset.zero), equals(titleOriginalOffset.translate(0.0, -200.0)));
      expect(content.localToGlobal(Offset.zero), equals(contentOriginalOffset.translate(0.0, -200.0)));

      // Dragging the content widget should scroll both the title
      // and the content widgets.
      await tester.drag(find.byKey(contentKey), const Offset(0.0, 200.0));
      expect(title.localToGlobal(Offset.zero), equals(titleOriginalOffset));
      expect(content.localToGlobal(Offset.zero), equals(contentOriginalOffset));
    });
  });

  testWidgets('Dialog with RouteSettings', (WidgetTester tester) async {
    late RouteSettings currentRouteSetting;

    await tester.pumpWidget(
      MaterialApp(
        navigatorObservers: <NavigatorObserver>[
          _ClosureNavigatorObserver(onDidChange: (Route<dynamic> newRoute) {
            currentRouteSetting = newRoute.settings;
          })
        ],
        home: const Material(
          child: Center(
            child: ElevatedButton(
              onPressed: null,
              child: Text('Go'),
            ),
          ),
        ),
      ),
    );

    final BuildContext context = tester.element(find.text('Go'));
    const RouteSettings exampleSetting = RouteSettings(name: 'simple');

    final Future<int?> result = showDialog<int>(
      context: context,
      builder: (BuildContext context) {
        return SimpleDialog(
          title: const Text('Title'),
          children: <Widget>[
            SimpleDialogOption(
              child: const Text('X'),
              onPressed: () {
                Navigator.of(context).pop();
              },
            ),
          ],
        );
      },
      routeSettings: exampleSetting,
    );

    await tester.pumpAndSettle();
    expect(find.text('Title'), findsOneWidget);
    expect(currentRouteSetting, exampleSetting);

    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();

    expect(await result, isNull);
    await tester.pumpAndSettle();
    expect(currentRouteSetting.name, '/');
  });
}

class DialogObserver extends NavigatorObserver {
  int dialogCount = 0;

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    if (route.toString().contains('_DialogRoute')) {
      dialogCount++;
    }
    super.didPush(route, previousRoute);
  }
}

class _ClosureNavigatorObserver extends NavigatorObserver {
  _ClosureNavigatorObserver({required this.onDidChange});

  final void Function(Route<dynamic> newRoute) onDidChange;

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(route);

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(previousRoute!);

  @override
  void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(previousRoute!);

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => onDidChange(newRoute!);
}
