// 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 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

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

void main() {
  // Pumps and ensures that the BottomSheet animates non-linearly.
  Future<void> checkNonLinearAnimation(WidgetTester tester) async {
    final Offset firstPosition = tester.getCenter(find.text('One'));
    await tester.pump(const Duration(milliseconds: 30));
    final Offset secondPosition = tester.getCenter(find.text('One'));
    await tester.pump(const Duration(milliseconds: 30));
    final Offset thirdPosition = tester.getCenter(find.text('One'));

    final double dyDelta1 = secondPosition.dy - firstPosition.dy;
    final double dyDelta2 = thirdPosition.dy - secondPosition.dy;

    // If the animation were linear, these two values would be the same.
    expect(dyDelta1, isNot(moreOrLessEquals(dyDelta2, epsilon: 0.1)));
  }

  testWidgets('Persistent draggableScrollableSheet localHistoryEntries test', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/110123
    Widget buildFrame(Widget? bottomSheet) {
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(),
          body: const Center(child: Text('body')),
          bottomSheet: bottomSheet,
          floatingActionButton: const FloatingActionButton(
            onPressed: null,
            child: Text('fab'),
          ),
        ),
      );
    }
    final Widget draggableScrollableSheet = DraggableScrollableSheet(
      expand: false,
      snap: true,
      initialChildSize: 0.3,
      minChildSize: 0.3,
      builder: (_, ScrollController controller) {
        return ListView.builder(
          itemExtent: 50.0,
          itemCount: 50,
          itemBuilder: (_, int index) => Text('Item $index'),
          controller: controller,
        );
      },
    );

    await tester.pumpWidget(buildFrame(draggableScrollableSheet));
    await tester.pumpAndSettle();

    expect(find.byType(BackButton).hitTestable(), findsNothing);

    await tester.drag(find.text('Item 2'), const Offset(0, -200.0));
    await tester.pumpAndSettle();
    // We've started to drag up, we should have a back button now for a11y
    expect(find.byType(BackButton).hitTestable(), findsOneWidget);

    await tester.fling(find.text('Item 2'), const Offset(0, 200.0), 2000.0);
    await tester.pumpAndSettle();
    // BackButton should be hidden
    expect(find.byType(BackButton).hitTestable(), findsNothing);

    // Show the back button again
    await tester.drag(find.text('Item 2'), const Offset(0, -200.0));
    await tester.pumpAndSettle();
    expect(find.byType(BackButton).hitTestable(), findsOneWidget);

    // Remove the draggableScrollableSheet should hide the back button
    await tester.pumpWidget(buildFrame(null));
    expect(find.byType(BackButton).hitTestable(), findsNothing);
  });

  // Regression test for https://github.com/flutter/flutter/issues/83668
  testWidgets('Scaffold.bottomSheet update test', (WidgetTester tester) async {
    Widget buildFrame(Widget? bottomSheet) {
      return MaterialApp(
        home: Scaffold(
          body: const Placeholder(),
          bottomSheet: bottomSheet,
        ),
      );
    }

    await tester.pumpWidget(buildFrame(const Text('I love Flutter!')));
    await tester.pumpWidget(buildFrame(null));

    // The disappearing animation has not yet been completed.
    await tester.pumpWidget(buildFrame(const Text('I love Flutter!')));
  });

  testWidgetsWithLeakTracking('Verify that a BottomSheet can be rebuilt with ScaffoldFeatureController.setState()', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
    int buildCount = 0;

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
      ),
    ));

    final PersistentBottomSheetController<void> bottomSheet = scaffoldKey.currentState!.showBottomSheet<void>((_) {
      return Builder(
        builder: (BuildContext context) {
          buildCount += 1;
          return Container(height: 200.0);
        },
      );
    });

    await tester.pump();
    expect(buildCount, equals(1));
    bottomSheet.setState!(() { });
    await tester.pump();
    expect(buildCount, equals(2));
  });

  testWidgetsWithLeakTracking('Verify that a persistent BottomSheet cannot be dismissed', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        body: const Center(child: Text('body')),
        bottomSheet: DraggableScrollableSheet(
          expand: false,
          builder: (_, ScrollController controller) {
            return ListView(
              controller: controller,
              shrinkWrap: true,
              children: const <Widget>[
                SizedBox(height: 100.0, child: Text('One')),
                SizedBox(height: 100.0, child: Text('Two')),
                SizedBox(height: 100.0, child: Text('Three')),
              ],
            );
          },
        ),
      ),
    ));

    await tester.pumpAndSettle();

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

    await tester.drag(find.text('Two'), const Offset(0.0, 400.0));
    await tester.pumpAndSettle();

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

  testWidgetsWithLeakTracking('Verify that a scrollable BottomSheet can be dismissed', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
      ),
    ));

    scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) {
      return ListView(
        shrinkWrap: true,
        primary: false,
        children: const <Widget>[
          SizedBox(height: 100.0, child: Text('One')),
          SizedBox(height: 100.0, child: Text('Two')),
          SizedBox(height: 100.0, child: Text('Three')),
        ],
      );
    });

    await tester.pumpAndSettle();

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

    await tester.drag(find.text('Two'), const Offset(0.0, 400.0));
    await tester.pumpAndSettle();

    expect(find.text('Two'), findsNothing);
  });

  testWidgetsWithLeakTracking('Verify DraggableScrollableSheet.shouldCloseOnMinExtent == false prevents dismissal', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
      ),
    ));

    scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) {
      return DraggableScrollableSheet(
        expand: false,
        shouldCloseOnMinExtent: false,
        builder: (_, ScrollController controller) {
          return ListView(
            controller: controller,
            shrinkWrap: true,
            children: const <Widget>[
              SizedBox(height: 100.0, child: Text('One')),
              SizedBox(height: 100.0, child: Text('Two')),
              SizedBox(height: 100.0, child: Text('Three')),
            ],
          );
        },
      );
    });

    await tester.pumpAndSettle();

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

    await tester.drag(find.text('Two'), const Offset(0.0, 400.0));
    await tester.pumpAndSettle();

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

  testWidgetsWithLeakTracking('Verify that a BottomSheet animates non-linearly', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
      ),
    ));

    scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) {
      return ListView(
        shrinkWrap: true,
        primary: false,
        children: const <Widget>[
          SizedBox(height: 100.0, child: Text('One')),
          SizedBox(height: 100.0, child: Text('Two')),
          SizedBox(height: 100.0, child: Text('Three')),
        ],
      );
    });
    await tester.pump();
    await checkNonLinearAnimation(tester);

    await tester.pumpAndSettle();

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

    await tester.drag(find.text('Two'), const Offset(0.0, 200.0));
    await checkNonLinearAnimation(tester);
    await tester.pumpAndSettle();

    expect(find.text('Two'), findsNothing);
  });

  testWidgets('Verify that a scrollControlled BottomSheet can be dismissed', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
      ),
    ));

    scaffoldKey.currentState!.showBottomSheet<void>(
      (BuildContext context) {
        return DraggableScrollableSheet(
          expand: false,
          builder: (_, ScrollController controller) {
            return ListView(
              shrinkWrap: true,
              controller: controller,
              children: const <Widget>[
                SizedBox(height: 100.0, child: Text('One')),
                SizedBox(height: 100.0, child: Text('Two')),
                SizedBox(height: 100.0, child: Text('Three')),
              ],
            );
          },
        );
      },
    );

    await tester.pumpAndSettle();

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

    await tester.drag(find.text('Two'), const Offset(0.0, 400.0));
    await tester.pumpAndSettle();

    expect(find.text('Two'), findsNothing);
  });

  testWidgetsWithLeakTracking('Verify that a persistent BottomSheet can fling up and hide the fab', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(),
          body: const Center(child: Text('body')),
          bottomSheet: DraggableScrollableSheet(
            expand: false,
            builder: (_, ScrollController controller) {
              return ListView.builder(
                itemExtent: 50.0,
                itemCount: 50,
                itemBuilder: (_, int index) => Text('Item $index'),
                controller: controller,
              );
            },
          ),
          floatingActionButton: const FloatingActionButton(
            onPressed: null,
            child: Text('fab'),
          ),
        ),
      ),
    );

    await tester.pumpAndSettle();

    expect(find.text('Item 2'), findsOneWidget);
    expect(find.text('Item 22'), findsNothing);
    expect(find.byType(FloatingActionButton), findsOneWidget);
    expect(find.byType(FloatingActionButton).hitTestable(), findsOneWidget);
    expect(find.byType(BackButton).hitTestable(), findsNothing);

    await tester.drag(find.text('Item 2'), const Offset(0, -20.0));
    await tester.pumpAndSettle();

    expect(find.text('Item 2'), findsOneWidget);
    expect(find.text('Item 22'), findsNothing);
    expect(find.byType(FloatingActionButton), findsOneWidget);
    expect(find.byType(FloatingActionButton).hitTestable(), findsOneWidget);

    await tester.fling(find.text('Item 2'), const Offset(0.0, -600.0), 2000.0);
    await tester.pumpAndSettle();

    expect(find.text('Item 2'), findsNothing);
    expect(find.text('Item 22'), findsOneWidget);
    expect(find.byType(FloatingActionButton), findsOneWidget);
    expect(find.byType(FloatingActionButton).hitTestable(), findsNothing);
  });

  testWidgets('Verify that a back button resets a persistent BottomSheet', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(),
          body: const Center(child: Text('body')),
          bottomSheet: DraggableScrollableSheet(
            expand: false,
            builder: (_, ScrollController controller) {
              return ListView.builder(
                itemExtent: 50.0,
                itemCount: 50,
                itemBuilder: (_, int index) => Text('Item $index'),
                controller: controller,
              );
            },
          ),
          floatingActionButton: const FloatingActionButton(
            onPressed: null,
            child: Text('fab'),
          ),
        ),
      ),
    );

    await tester.pumpAndSettle();

    expect(find.text('Item 2'), findsOneWidget);
    expect(find.text('Item 22'), findsNothing);
    expect(find.byType(BackButton).hitTestable(), findsNothing);

    await tester.drag(find.text('Item 2'), const Offset(0, -20.0));
    await tester.pumpAndSettle();

    expect(find.text('Item 2'), findsOneWidget);
    expect(find.text('Item 22'), findsNothing);
    // We've started to drag up, we should have a back button now for a11y
    expect(find.byType(BackButton).hitTestable(), findsOneWidget);

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

    expect(find.byType(BackButton).hitTestable(), findsNothing);
    expect(find.text('Item 2'), findsOneWidget);
    expect(find.text('Item 22'), findsNothing);

    await tester.fling(find.text('Item 2'), const Offset(0.0, -600.0), 2000.0);
    await tester.pumpAndSettle();

    expect(find.text('Item 2'), findsNothing);
    expect(find.text('Item 22'), findsOneWidget);
    expect(find.byType(BackButton).hitTestable(), findsOneWidget);

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

    expect(find.byType(BackButton).hitTestable(), findsNothing);
    expect(find.text('Item 2'), findsOneWidget);
    expect(find.text('Item 22'), findsNothing);
  });

  testWidgetsWithLeakTracking('Verify that a scrollable BottomSheet hides the fab when scrolled up', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
        floatingActionButton: const FloatingActionButton(
          onPressed: null,
          child: Text('fab'),
        ),
      ),
    ));

    scaffoldKey.currentState!.showBottomSheet<void>(
      (BuildContext context) {
        return DraggableScrollableSheet(
          expand: false,
          builder: (_, ScrollController controller) {
            return ListView(
              controller: controller,
              shrinkWrap: true,
              children: const <Widget>[
                SizedBox(height: 100.0, child: Text('One')),
                SizedBox(height: 100.0, child: Text('Two')),
                SizedBox(height: 100.0, child: Text('Three')),
                SizedBox(height: 100.0, child: Text('Three')),
                SizedBox(height: 100.0, child: Text('Three')),
                SizedBox(height: 100.0, child: Text('Three')),
                SizedBox(height: 100.0, child: Text('Three')),
                SizedBox(height: 100.0, child: Text('Three')),
                SizedBox(height: 100.0, child: Text('Three')),
                SizedBox(height: 100.0, child: Text('Three')),
                SizedBox(height: 100.0, child: Text('Three')),
              ],
            );
          },
        );
      },
    );

    await tester.pumpAndSettle();

    expect(find.text('Two'), findsOneWidget);
    expect(find.byType(FloatingActionButton).hitTestable(), findsOneWidget);

    await tester.drag(find.text('Two'), const Offset(0.0, -600.0));
    await tester.pumpAndSettle();

    expect(find.text('Two'), findsOneWidget);
    expect(find.byType(FloatingActionButton), findsOneWidget);
    expect(find.byType(FloatingActionButton).hitTestable(), findsNothing);
  });

  testWidgetsWithLeakTracking('showBottomSheet()', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        body: Placeholder(key: key),
      ),
    ));

    int buildCount = 0;
    showBottomSheet<void>(
      context: key.currentContext!,
      builder: (BuildContext context) {
        return Builder(
          builder: (BuildContext context) {
            buildCount += 1;
            return Container(height: 200.0);
          },
        );
      },
    );
    await tester.pump();
    expect(buildCount, equals(1));
  });

  testWidgetsWithLeakTracking('Scaffold removes top MediaQuery padding', (WidgetTester tester) async {
    late BuildContext scaffoldContext;
    late BuildContext bottomSheetContext;

    await tester.pumpWidget(MaterialApp(
      home: MediaQuery(
        data: const MediaQueryData(
          padding: EdgeInsets.all(50.0),
        ),
        child: Scaffold(
          resizeToAvoidBottomInset: false,
          body: Builder(
            builder: (BuildContext context) {
              scaffoldContext = context;
              return Container();
            },
          ),
        ),
      ),
    ));

    await tester.pump();

    showBottomSheet<void>(
      context: scaffoldContext,
      builder: (BuildContext context) {
        bottomSheetContext = context;
        return Container();
      },
    );

    await tester.pump();

    expect(
      MediaQuery.of(bottomSheetContext).padding,
      const EdgeInsets.only(
        bottom: 50.0,
        left: 50.0,
        right: 50.0,
      ),
    );
  });

  testWidgets('Scaffold.bottomSheet', (WidgetTester tester) async {
    final Key bottomSheetKey = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(useMaterial3: false),
        home: Scaffold(
          body: const Placeholder(),
          bottomSheet: Container(
            key: bottomSheetKey,
            alignment: Alignment.center,
            height: 200.0,
            child: Builder(
              builder: (BuildContext context) {
                return ElevatedButton(
                  child: const Text('showModalBottomSheet'),
                  onPressed: () {
                    showModalBottomSheet<void>(
                      context: context,
                      builder: (BuildContext context) => const Text('modal bottom sheet'),
                    );
                  },
                );
              },
            ),
          ),
        ),
      ),
    );

    expect(find.text('showModalBottomSheet'), findsOneWidget);
    expect(tester.getSize(find.byKey(bottomSheetKey)), const Size(800.0, 200.0));
    expect(tester.getTopLeft(find.byKey(bottomSheetKey)), const Offset(0.0, 400.0));

    // Show the modal bottomSheet
    await tester.tap(find.text('showModalBottomSheet'));
    await tester.pumpAndSettle();
    expect(find.text('modal bottom sheet'), findsOneWidget);

    // Dismiss the modal bottomSheet by tapping above the sheet
    await tester.tapAt(const Offset(20.0, 20.0));
    await tester.pumpAndSettle();
    expect(find.text('modal bottom sheet'), findsNothing);
    expect(find.text('showModalBottomSheet'), findsOneWidget);

    // Remove the persistent bottomSheet
    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(
          body: Placeholder(),
        ),
      ),
    );
    await tester.pumpAndSettle();
    expect(find.text('showModalBottomSheet'), findsNothing);
    expect(find.byKey(bottomSheetKey), findsNothing);
  });

  // Regression test for https://github.com/flutter/flutter/issues/71435
  testWidgetsWithLeakTracking(
    'Scaffold.bottomSheet should be updated without creating a new RO'
    ' when the new widget has the same key and type.',
    (WidgetTester tester) async {
      Widget buildFrame(String text) {
        return MaterialApp(
          home: Scaffold(
            body: const Placeholder(),
            bottomSheet: Text(text),
          ),
        );
      }

      await tester.pumpWidget(buildFrame('I love Flutter!'));
      final RenderParagraph renderBeforeUpdate = tester.renderObject(find.text('I love Flutter!'));

      await tester.pumpWidget(buildFrame('Flutter is the best!'));
      await tester.pumpAndSettle();
      final RenderParagraph renderAfterUpdate = tester.renderObject(find.text('Flutter is the best!'));

      expect(renderBeforeUpdate, renderAfterUpdate);
    },
  );

  testWidgetsWithLeakTracking('Verify that visual properties are passed through', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
    const Color color = Colors.pink;
    const double elevation = 9.0;
    const ShapeBorder shape = BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12)));
    const Clip clipBehavior = Clip.antiAlias;

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
      ),
    ));

    scaffoldKey.currentState!.showBottomSheet<void>((BuildContext context) {
      return ListView(
        shrinkWrap: true,
        primary: false,
        children: const <Widget>[
          SizedBox(height: 100.0, child: Text('One')),
          SizedBox(height: 100.0, child: Text('Two')),
          SizedBox(height: 100.0, child: Text('Three')),
        ],
      );
    }, backgroundColor: color, elevation: elevation, shape: shape, clipBehavior: clipBehavior);

    await tester.pumpAndSettle();

    final BottomSheet bottomSheet = tester.widget(find.byType(BottomSheet));
    expect(bottomSheet.backgroundColor, color);
    expect(bottomSheet.elevation, elevation);
    expect(bottomSheet.shape, shape);
    expect(bottomSheet.clipBehavior, clipBehavior);
  });

  testWidgetsWithLeakTracking('PersistentBottomSheetController.close dismisses the bottom sheet', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
      ),
    ));

    final PersistentBottomSheetController<void> bottomSheet = scaffoldKey.currentState!.showBottomSheet<void>((_) {
      return Builder(
        builder: (BuildContext context) {
          return Container(height: 200.0);
        },
      );
    });

    await tester.pump();
    expect(find.byType(BottomSheet), findsOneWidget);

    bottomSheet.close();
    await tester.pump();
    expect(find.byType(BottomSheet), findsNothing);
  });
}
