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

import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';

import '../rendering/mock_canvas.dart';

final Matcher doesNotOverscroll = isNot(paints..circle());

Future<void> slowDrag(WidgetTester tester, Offset start, Offset offset) async {
  final TestGesture gesture = await tester.startGesture(start);
  for (int index = 0; index < 10; index += 1) {
    await gesture.moveBy(offset);
    await tester.pump(const Duration(milliseconds: 20));
  }
  await gesture.up();
}

void main() {
  testWidgets('Overscroll indicator color', (WidgetTester tester) async {
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          slivers: <Widget>[
            SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
          ],
        ),
      ),
    );
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));

    expect(painter, doesNotOverscroll);

    // the scroll gesture from tester.scroll happens in zero time, so nothing should appear:
    await tester.drag(find.byType(Scrollable), const Offset(0.0, 100.0));
    expect(painter, doesNotOverscroll);
    await tester.pump(); // allow the ticker to register itself
    expect(painter, doesNotOverscroll);
    await tester.pump(const Duration(milliseconds: 100)); // animate
    expect(painter, doesNotOverscroll);

    final TestGesture gesture = await tester.startGesture(const Offset(200.0, 200.0));
    await tester.pump(const Duration(milliseconds: 100)); // animate
    expect(painter, doesNotOverscroll);
    await gesture.up();
    expect(painter, doesNotOverscroll);

    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
    expect(painter, paints..circle(color: const Color(0x0DFFFFFF)));

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(painter, doesNotOverscroll);
  });

  testWidgets('Nested scrollable', (WidgetTester tester) async {
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: GlowingOverscrollIndicator(
          axisDirection: AxisDirection.down,
          color: const Color(0x0DFFFFFF),
          notificationPredicate: (ScrollNotification notification) => notification.depth == 1,
          child: const SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: SizedBox(
                width: 600.0,
                child: CustomScrollView(
                  slivers: <Widget>[
                    SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
                  ],
                ),
              ),
            ),
          ),
        ),
      );

    final RenderObject outerPainter = tester.renderObject(find.byType(CustomPaint).first);
    final RenderObject innerPainter = tester.renderObject(find.byType(CustomPaint).last);

    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
    expect(outerPainter, paints..circle());
    expect(innerPainter, paints..circle());
  });

  testWidgets('Overscroll indicator changes side when you drag on the other side', (WidgetTester tester) async {
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          slivers: <Widget>[
            SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
          ],
        ),
      ),
    );
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));

    await slowDrag(tester, const Offset(400.0, 200.0), const Offset(0.0, 10.0));
    expect(painter, paints..circle(x: 400.0));
    await slowDrag(tester, const Offset(100.0, 200.0), const Offset(0.0, 10.0));
    expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
      if (method != #drawCircle) {
        return false;
      }
      final Offset center = arguments[0] as Offset;
      if (center.dx < 400.0) {
        return true;
      }
      throw 'Dragging on left hand side did not overscroll on left hand side.';
    }));
    await slowDrag(tester, const Offset(700.0, 200.0), const Offset(0.0, 10.0));
    expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
      if (method != #drawCircle) {
        return false;
      }
      final Offset center = arguments[0] as Offset;
      if (center.dx > 400.0) {
        return true;
      }
      throw 'Dragging on right hand side did not overscroll on right hand side.';
    }));

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(painter, doesNotOverscroll);
  });

  testWidgets('Overscroll indicator changes side when you shift sides', (WidgetTester tester) async {
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          slivers: <Widget>[
            SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
          ],
        ),
      ),
    );
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    final TestGesture gesture = await tester.startGesture(const Offset(300.0, 200.0));
    await gesture.moveBy(const Offset(0.0, 10.0));
    await tester.pump(const Duration(milliseconds: 20));
    double oldX = 0.0;
    for (int index = 0; index < 10; index += 1) {
      await gesture.moveBy(const Offset(50.0, 50.0));
      await tester.pump(const Duration(milliseconds: 20));
      expect(painter, paints..something((Symbol method, List<dynamic> arguments) {
        if (method != #drawCircle) {
          return false;
        }
        final Offset center = arguments[0] as Offset;
        if (center.dx <= oldX) {
          throw 'Sliding to the right did not make the center of the radius slide to the right.';
        }
        oldX = center.dx;
        return true;
      }));
    }
    await gesture.up();

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(painter, doesNotOverscroll);
  });

  group("Flipping direction of scrollable doesn't change overscroll behavior", () {
    testWidgets('down', (WidgetTester tester) async {
      await tester.pumpWidget(
        const Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            physics: AlwaysScrollableScrollPhysics(),
            slivers: <Widget>[
              SliverToBoxAdapter(child: SizedBox(height: 20.0)),
            ],
          ),
        ),
      );
      final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
      await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
      expect(painter, paints..save()..circle()..restore()..save()..scale(y: -1.0)..restore()..restore());

      await tester.pumpAndSettle(const Duration(seconds: 1));
      expect(painter, doesNotOverscroll);
    });

    testWidgets('up', (WidgetTester tester) async {
      await tester.pumpWidget(
        const Directionality(
          textDirection: TextDirection.ltr,
          child: CustomScrollView(
            reverse: true,
            physics: AlwaysScrollableScrollPhysics(),
            slivers: <Widget>[
              SliverToBoxAdapter(child: SizedBox(height: 20.0)),
            ],
          ),
        ),
      );
      final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
      await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
      expect(painter, paints..save()..scale(y: -1.0)..restore()..save()..circle()..restore()..restore());

      await tester.pumpAndSettle(const Duration(seconds: 1));
      expect(painter, doesNotOverscroll);
    });
  });

  testWidgets('Overscroll in both directions', (WidgetTester tester) async {
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          physics: AlwaysScrollableScrollPhysics(),
          slivers: <Widget>[
            SliverToBoxAdapter(child: SizedBox(height: 20.0)),
          ],
        ),
      ),
    );
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
    expect(painter, paints..circle());
    expect(painter, isNot(paints..circle()..circle()));
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -5.0));
    expect(painter, paints..circle()..circle());

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(painter, doesNotOverscroll);
  });

  testWidgets('Overscroll horizontally', (WidgetTester tester) async {
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: CustomScrollView(
          scrollDirection: Axis.horizontal,
          physics: AlwaysScrollableScrollPhysics(),
          slivers: <Widget>[
            SliverToBoxAdapter(child: SizedBox(height: 20.0)),
          ],
        ),
      ),
    );
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0));
    expect(painter, paints..rotate(angle: math.pi / 2.0)..circle()..saveRestore());
    expect(painter, isNot(paints..circle()..circle()));
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(-5.0, 0.0));
    expect(
      painter,
      paints
        ..rotate(angle: math.pi / 2.0)
        ..circle()
        ..rotate(angle: math.pi / 2.0)
        ..circle(),
    );

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(painter, doesNotOverscroll);
  });

  testWidgets('Nested overscrolls do not throw exceptions', (WidgetTester tester) async {
    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: PageView(
        children: <Widget>[
          ListView(
            children: <Widget>[
              Container(
                width: 2000.0,
                height: 2000.0,
                color: const Color(0xFF00FF00),
              ),
            ],
          ),
        ],
      ),
    ));

    await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 2000.0));
    await tester.pumpAndSettle();
  });

  testWidgets('Changing settings', (WidgetTester tester) async {
    RenderObject painter;

    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: ScrollConfiguration(
          behavior: TestScrollBehavior1(),
          child: CustomScrollView(
            scrollDirection: Axis.horizontal,
            physics: AlwaysScrollableScrollPhysics(),
            reverse: true,
            slivers: <Widget>[
              SliverToBoxAdapter(child: SizedBox(height: 20.0)),
            ],
          ),
        ),
      ),
    );
    painter = tester.renderObject(find.byType(CustomPaint));
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0));
    expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A00FF00)));
    expect(painter, isNot(paints..circle()..circle()));

    await tester.pumpAndSettle(const Duration(seconds: 1));
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: ScrollConfiguration(
          behavior: TestScrollBehavior2(),
          child: CustomScrollView(
            scrollDirection: Axis.horizontal,
            physics: AlwaysScrollableScrollPhysics(),
            slivers: <Widget>[
              SliverToBoxAdapter(child: SizedBox(height: 20.0)),
            ],
          ),
        ),
      ),
    );
    painter = tester.renderObject(find.byType(CustomPaint));
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(5.0, 0.0));
    expect(painter, paints..rotate(angle: math.pi / 2.0)..circle(color: const Color(0x0A0000FF))..saveRestore());
    expect(painter, isNot(paints..circle()..circle()));
  });

  testWidgets('CustomScrollView overscroll indicator works if there is sliver before center', (WidgetTester tester) async {
    final Key centerKey = UniqueKey();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ScrollConfiguration(
          behavior: const TestScrollBehavior2(),
          child: CustomScrollView(
            center: centerKey,
            physics: const AlwaysScrollableScrollPhysics(),
            slivers: <Widget>[
              SliverList(
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) => Text('First sliver $index'),
                  childCount: 2,
                ),
              ),
              SliverList(
                key: centerKey,
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) => Text('Second sliver $index'),
                  childCount: 5,
                ),
              ),
            ],
          ),
        ),
      ),
    );

    expect(find.text('First sliver 1'), findsNothing);

    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 300.0));
    expect(find.text('First sliver 1'), findsOneWidget);
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    // The scroll offset and paint extend should cancel out each other.
    expect(painter, paints..save()..translate(y: 0.0)..scale()..circle());
  });

  testWidgets('CustomScrollView overscroll indicator works well with [CustomScrollView.center] and [OverscrollIndicatorNotification.paintOffset]', (WidgetTester tester) async {
    final Key centerKey = UniqueKey();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ScrollConfiguration(
          behavior: const TestScrollBehavior2(),
          child: NotificationListener<OverscrollIndicatorNotification>(
            onNotification: (OverscrollIndicatorNotification notification) {
              if (notification.leading) {
                notification.paintOffset = 50.0;
              }
              return false;
            },
            child: CustomScrollView(
              center: centerKey,
              physics: const AlwaysScrollableScrollPhysics(),
              slivers: <Widget>[
                SliverList(
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) => Text('First sliver $index'),
                    childCount: 2,
                  ),
                ),
                SliverList(
                  key: centerKey,
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) => Text('Second sliver $index'),
                    childCount: 5,
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );

    expect(find.text('First sliver 1'), findsNothing);

    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0)); // offset will be magnified ten times
    expect(find.text('First sliver 1'), findsOneWidget);
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting.
    expect(painter, paints..save()..translate(y: 50.0)..scale()..circle());
  });

  testWidgets('The OverscrollIndicator should not overflow the scrollable view edge', (WidgetTester tester) async {
    // Regressing test for https://github.com/flutter/flutter/issues/64149
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: NotificationListener<OverscrollIndicatorNotification>(
          onNotification: (OverscrollIndicatorNotification notification) {
            notification.paintOffset = 50.0; // both the leading and trailing indicator have a 50.0 pixels offset.
            return false;
          },
          child: const CustomScrollView(
            slivers: <Widget>[
              SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
            ],
          ),
        ),
      ),
    );
    final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
    expect(painter, paints..save()..translate(y: 50.0)..scale()..circle());
    // Reverse scroll (30 pixels), and the offset < notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -30.0));
    await tester.pump();
    // The OverscrollIndicator should move with the CustomScrollView.
    expect(painter, paints..save()..translate(y: 50.0 - 30.0)..scale()..circle());

    // Reverse scroll (30+20 pixels) and offset == notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -20.0));
    await tester.pump();
    expect(painter, paints..save()..translate(y: 50.0 - 50.0)..scale()..circle());

    // Reverse scroll (30+20+10 pixels) and offset > notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -10.0));
    await tester.pump();
    // The OverscrollIndicator should not overflow the CustomScrollView's edge.
    expect(painter, paints..save()..translate(y: 50.0 - 50.0)..scale()..circle());

    await tester.pumpAndSettle(); // Finish the leading indicator.

    // trigger the trailing indicator
    await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -200.0));
    expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0)..scale()..circle());

    // Reverse scroll (30 pixels), and the offset < notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 30.0));
    await tester.pump();
    // The OverscrollIndicator should move with the CustomScrollView.
    expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 30.0)..scale()..circle());

    // Reverse scroll (30+20 pixels) and offset == notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 20.0));
    await tester.pump();
    expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 50.0)..scale()..circle());

    // Reverse scroll (30+20+10 pixels) and offset > notification.paintOffset.
    await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 10.0));
    await tester.pump();
    // The OverscrollIndicator should not overflow the CustomScrollView's edge.
    expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 50.0)..scale()..circle());
  });

  group('[OverscrollIndicatorNotification.paintOffset] test', () {
    testWidgets('Leading', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: NotificationListener<OverscrollIndicatorNotification>(
            onNotification: (OverscrollIndicatorNotification notification) {
              if (notification.leading) {
                notification.paintOffset = 50.0;
              }
              return false;
            },
            child: const CustomScrollView(
              slivers: <Widget>[
                SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
              ],
            ),
          ),
        ),
      );
      final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
      await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, 5.0));
      // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting.
      expect(painter, paints..save()..translate(y: 50.0)..scale()..circle());
      // Reverse scroll direction.
      await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, -30.0));
      await tester.pump();
      // The OverscrollIndicator should move with the CustomScrollView.
      expect(painter, paints..save()..translate(y: 50.0 - 30.0)..scale()..circle());
    });

    testWidgets('Trailing', (WidgetTester tester) async {
      await tester.pumpWidget(
        Directionality(
          textDirection: TextDirection.ltr,
          child: NotificationListener<OverscrollIndicatorNotification>(
            onNotification: (OverscrollIndicatorNotification notification) {
              if (!notification.leading) {
                notification.paintOffset = 50.0;
              }
              return false;
            },
            child: const CustomScrollView(
              slivers: <Widget>[
                SliverToBoxAdapter(child: SizedBox(height: 2000.0)),
              ],
            ),
          ),
        ),
      );
      final RenderObject painter = tester.renderObject(find.byType(CustomPaint));
      await tester.dragFrom(const Offset(200.0, 200.0), const Offset(200.0, -10000.0));
      await tester.pump();
      await slowDrag(tester, const Offset(200.0, 200.0), const Offset(0.0, -5.0));
      // The OverscrollIndicator should respect the [OverscrollIndicatorNotification.paintOffset] setting.
      expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0)..scale()..circle());
      // Reverse scroll direction.
      await tester.dragFrom(const Offset(200.0, 200.0), const Offset(0.0, 30.0));
      await tester.pump();
      // The OverscrollIndicator should move with the CustomScrollView.
      expect(painter, paints..scale(y: -1.0)..save()..translate(y: 50.0 - 30.0)..scale()..circle());
    });
  });
}

class TestScrollBehavior1 extends ScrollBehavior {
  const TestScrollBehavior1();

  @override
  Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
    return GlowingOverscrollIndicator(
      axisDirection: details.direction,
      color: const Color(0xFF00FF00),
      child: child,
    );
  }
}

class TestScrollBehavior2 extends ScrollBehavior {
  const TestScrollBehavior2();

  @override
  Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
    return GlowingOverscrollIndicator(
      axisDirection: details.direction,
      color: const Color(0xFF0000FF),
      child: child,
    );
  }
}
