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

void main() {
  Widget _boilerplate(VoidCallback? onButtonPressed, {
    int itemCount = 100,
    double initialChildSize = .5,
    double maxChildSize = 1.0,
    double minChildSize = .25,
    double? itemExtent,
    Key? containerKey,
    NotificationListenerCallback<ScrollNotification>? onScrollNotification,
  }) {
    return Directionality(
      textDirection: TextDirection.ltr,
      child: Stack(
        children: <Widget>[
          TextButton(
            child: const Text('TapHere'),
            onPressed: onButtonPressed,
          ),
          DraggableScrollableSheet(
            maxChildSize: maxChildSize,
            minChildSize: minChildSize,
            initialChildSize: initialChildSize,
            builder: (BuildContext context, ScrollController scrollController) {
              return NotificationListener<ScrollNotification>(
                onNotification: onScrollNotification,
                child: Container(
                  key: containerKey,
                  color: const Color(0xFFABCDEF),
                  child: ListView.builder(
                    controller: scrollController,
                    itemExtent: itemExtent,
                    itemCount: itemCount,
                    itemBuilder: (BuildContext context, int index) => Text('Item $index'),
                  ),
                ),
              );
            },
          ),
        ],
      ),
    );
  }

  testWidgets('Scrolls correct amount when maxChildSize < 1.0', (WidgetTester tester) async {
    const Key key = ValueKey<String>('container');
    await tester.pumpWidget(_boilerplate(
      null,
      maxChildSize: .6,
      initialChildSize: .25,
      itemExtent: 25.0,
      containerKey: key,
    ));

    expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 450.0, 800.0, 600.0));
    await tester.drag(find.text('Item 5'), const Offset(0, -125));
    await tester.pumpAndSettle();
    expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 325.0, 800.0, 600.0));
  });

  testWidgets('Scrolls correct amount when maxChildSize == 1.0', (WidgetTester tester) async {
    const Key key = ValueKey<String>('container');
    await tester.pumpWidget(_boilerplate(
      null,
      maxChildSize: 1.0,
      initialChildSize: .25,
      itemExtent: 25.0,
      containerKey: key,
    ));

    expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 450.0, 800.0, 600.0));
    await tester.drag(find.text('Item 5'), const Offset(0, -125));
    await tester.pumpAndSettle();
    expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0.0, 325.0, 800.0, 600.0));
  });

  for (final TargetPlatform platform in TargetPlatform.values) {
    group('$platform Scroll Physics', () {
      debugDefaultTargetPlatformOverride = platform;

      testWidgets('Can be dragged up without covering its container', (WidgetTester tester) async {
        int taps = 0;
        await tester.pumpWidget(_boilerplate(() => taps++));

        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 31'), findsNothing);

        await tester.drag(find.text('Item 1'), const Offset(0, -200));
        await tester.pumpAndSettle();
        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 2);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 31'), findsOneWidget);
      }, variant: TargetPlatformVariant.all());

      testWidgets('Can be dragged down when not full height', (WidgetTester tester) async {
        await tester.pumpWidget(_boilerplate(null));
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 36'), findsNothing);

        await tester.drag(find.text('Item 1'), const Offset(0, 325));
        await tester.pumpAndSettle();
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsNothing);
        expect(find.text('Item 36'), findsNothing);
      }, variant: TargetPlatformVariant.all());

      testWidgets('Can be dragged down when list is shorter than full height', (WidgetTester tester) async {
        await tester.pumpWidget(_boilerplate(null, itemCount: 30, initialChildSize: .25));

        expect(find.text('Item 1').hitTestable(), findsOneWidget);
        expect(find.text('Item 29').hitTestable(), findsNothing);

        await tester.drag(find.text('Item 1'), const Offset(0, -325));
        await tester.pumpAndSettle();
        expect(find.text('Item 1').hitTestable(), findsOneWidget);
        expect(find.text('Item 29').hitTestable(), findsOneWidget);

        await tester.drag(find.text('Item 1'), const Offset(0, 325));
        await tester.pumpAndSettle();
        expect(find.text('Item 1').hitTestable(), findsOneWidget);
        expect(find.text('Item 29').hitTestable(), findsNothing);
      }, variant: TargetPlatformVariant.all());

      testWidgets('Can be dragged up and cover its container and scroll in single motion, and then dragged back down', (WidgetTester tester) async {
        int taps = 0;
        await tester.pumpWidget(_boilerplate(() => taps++));

        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 36'), findsNothing);

        await tester.drag(find.text('Item 1'), const Offset(0, -325));
        await tester.pumpAndSettle();
        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 36'), findsOneWidget);

        await tester.dragFrom(const Offset(20, 20), const Offset(0, 325));
        await tester.pumpAndSettle();
        await tester.tap(find.text('TapHere'));
        expect(taps, 2);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 18'), findsOneWidget);
        expect(find.text('Item 36'), findsNothing);
      }, variant: TargetPlatformVariant.all());

      testWidgets('Can be flung up gently', (WidgetTester tester) async {
        int taps = 0;
        await tester.pumpWidget(_boilerplate(() => taps++));

        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 36'), findsNothing);
        expect(find.text('Item 70'), findsNothing);

        await tester.fling(find.text('Item 1'), const Offset(0, -200), 350);
        await tester.pumpAndSettle();
        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 2);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 36'), findsOneWidget);
        expect(find.text('Item 70'), findsNothing);
      }, variant: TargetPlatformVariant.all());

      testWidgets('Can be flung up', (WidgetTester tester) async {
        int taps = 0;
        await tester.pumpWidget(_boilerplate(() => taps++));

        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 70'), findsNothing);

        await tester.fling(find.text('Item 1'), const Offset(0, -200), 2000);
        await tester.pumpAndSettle();
        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsNothing);
        expect(find.text('Item 21'), findsNothing);
        expect(find.text('Item 70'), findsOneWidget);
      }, variant: TargetPlatformVariant.all());

      testWidgets('Can be flung down when not full height', (WidgetTester tester) async {
        await tester.pumpWidget(_boilerplate(null));
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 36'), findsNothing);

        await tester.fling(find.text('Item 1'), const Offset(0, 325), 2000);
        await tester.pumpAndSettle();
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsNothing);
        expect(find.text('Item 36'), findsNothing);
      }, variant: TargetPlatformVariant.all());

      testWidgets('Can be flung up and then back down', (WidgetTester tester) async {
        int taps = 0;
        await tester.pumpWidget(_boilerplate(() => taps++));

        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 70'), findsNothing);

        await tester.fling(find.text('Item 1'), const Offset(0, -200), 2000);
        await tester.pumpAndSettle();
        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsNothing);
        expect(find.text('Item 21'), findsNothing);
        expect(find.text('Item 70'), findsOneWidget);

        await tester.fling(find.text('Item 70'), const Offset(0, 200), 2000);
        await tester.pumpAndSettle();
        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 1);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsOneWidget);
        expect(find.text('Item 70'), findsNothing);

        await tester.fling(find.text('Item 1'), const Offset(0, 200), 2000);
        await tester.pumpAndSettle();
        expect(find.text('TapHere'), findsOneWidget);
        await tester.tap(find.text('TapHere'));
        expect(taps, 2);
        expect(find.text('Item 1'), findsOneWidget);
        expect(find.text('Item 21'), findsNothing);
        expect(find.text('Item 70'), findsNothing);
      }, variant: TargetPlatformVariant.all());

      debugDefaultTargetPlatformOverride = null;
    });

    testWidgets('ScrollNotification correctly dispatched when flung without covering its container', (WidgetTester tester) async {
      final List<Type> notificationTypes = <Type>[];
      await tester.pumpWidget(_boilerplate(
        null,
        onScrollNotification: (ScrollNotification notification) {
          notificationTypes.add(notification.runtimeType);
          return false;
        },
      ));

      await tester.fling(find.text('Item 1'), const Offset(0, -200), 200);
      await tester.pumpAndSettle();

      // TODO(itome): Make sure UserScrollNotification and ScrollUpdateNotification are called correctly.
      final List<Type> types = <Type>[
        ScrollStartNotification,
        ScrollEndNotification,
      ];
      expect(notificationTypes, equals(types));
    });

    testWidgets('ScrollNotification correctly dispatched when flung with contents scroll', (WidgetTester tester) async {
      final List<Type> notificationTypes = <Type>[];
      await tester.pumpWidget(_boilerplate(
        null,
        onScrollNotification: (ScrollNotification notification) {
          notificationTypes.add(notification.runtimeType);
          return false;
        },
      ));

      await tester.flingFrom(const Offset(0, 325), const Offset(0, -325), 200);
      await tester.pumpAndSettle();

      final List<Type> types = <Type>[
        ScrollStartNotification,
        UserScrollNotification,
        ...List<Type>.filled(5, ScrollUpdateNotification),
        ScrollEndNotification,
        UserScrollNotification,
      ];
      expect(notificationTypes, types);
    });
  }

  testWidgets('Builder is not called excessively', (WidgetTester tester) async {
    int buildCount = 0;
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Stack(
        children: <Widget>[
          DraggableScrollableSheet(
            builder: (BuildContext context, ScrollController scrollController) {
              buildCount += 1;
              return Container(
                color: const Color(0xFFABCDEF),
                child: ListView.builder(
                  controller: scrollController,
                  itemExtent: 100,
                  itemCount: 100,
                  itemBuilder: (BuildContext context, int index) => Text('Item $index'),
                ),
              );
            },
          ),
        ],
      ),
    ));
    expect(buildCount, 1);
    await tester.flingFrom(const Offset(0, 325), const Offset(0, -325), 200);
    expect(buildCount, 1);
    await tester.pumpAndSettle();
    expect(buildCount, 1);
  });

  testWidgets('Builder is called if widget updates', (WidgetTester tester) async {
    int buildCount = 0;
    final GlobalKey key = GlobalKey();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Stack(
        children: <Widget>[
          DraggableScrollableSheet(
            key: key,
            builder: (BuildContext context, ScrollController scrollController) {
              buildCount += 1;
              return Container(
                color: const Color(0xFFABCDEF),
                child: ListView.builder(
                  controller: scrollController,
                  itemExtent: 100,
                  itemCount: 100,
                  itemBuilder: (BuildContext context, int index) => Text('Item $index'),
                ),
              );
            },
          ),
        ],
      ),
    ));
    expect(buildCount, 1);
    expect(find.text('Item 1'), findsOneWidget);

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Stack(
        children: <Widget>[
          DraggableScrollableSheet(
            key: key,
            builder: (BuildContext context, ScrollController scrollController) {
              buildCount += 1;
              return Container(
                color: const Color(0xFFFEDCBA),
                child: ListView.builder(
                  controller: scrollController,
                  itemExtent: 50,
                  itemCount: 100,
                  itemBuilder: (BuildContext context, int index) => Text('New Item $index'),
                ),
              );
            },
          ),
        ],
      ),
    ));
    expect(buildCount, 2);
    expect(find.text('Item 1'), findsNothing);
    expect(find.text('New Item 1'), findsOneWidget);
  });

  testWidgets('Changes to min/max/initial child size are respected', (WidgetTester tester) async {
    final GlobalKey key = GlobalKey();
    final Key childKey = UniqueKey();
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Stack(
        children: <Widget>[
          DraggableScrollableSheet(
            key: key,
            minChildSize: .25,
            maxChildSize: 1.0,
            initialChildSize: .5,
            builder: (BuildContext context, ScrollController scrollController) {
              return Container(
                key: childKey,
                color: const Color(0xFFABCDEF),
                child: ListView.builder(
                  controller: scrollController,
                  itemExtent: 100,
                  itemCount: 100,
                  itemBuilder: (BuildContext context, int index) => Text('Item $index'),
                ),
              );
            },
          ),
        ],
      ),
    ));
    expect(find.text('Item 1'), findsOneWidget);
    expect(tester.getRect(find.byKey(childKey)), const Rect.fromLTRB(0, 300, 800, 600));

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Stack(
        children: <Widget>[
          DraggableScrollableSheet(
            key: key,
            minChildSize: .5,
            maxChildSize: .75,
            initialChildSize: .6,
            builder: (BuildContext context, ScrollController scrollController) {
              return Container(
                key: childKey,
                color: const Color(0xFFFEDCBA),
                child: ListView.builder(
                  controller: scrollController,
                  itemExtent: 50,
                  itemCount: 100,
                  itemBuilder: (BuildContext context, int index) => Text('New Item $index'),
                ),
              );
            },
          ),
        ],
      ),
    ));
    expect(find.text('New Item 1'), findsOneWidget);
    expect(tester.getRect(find.byKey(childKey)), const Rect.fromLTRB(0, 240, 800, 600));
  });
}
