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

void main() {
  testWidgets('Does not animate if already at target position', (WidgetTester tester) async {
    final ScrollController controller = ScrollController();
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView(
          controller: controller,
          children: List<Widget>.generate(
            80,
            (int i) => Text('$i', textDirection: TextDirection.ltr),
          ),
        ),
      ),
    );

    expectNoAnimation();
    final double currentPosition = controller.position.pixels;
    controller.position.animateTo(
      currentPosition,
      duration: const Duration(seconds: 10),
      curve: Curves.linear,
    );

    expectNoAnimation();
    expect(controller.position.pixels, currentPosition);
  });

  testWidgets('Does not animate if already at target position within tolerance', (
    WidgetTester tester,
  ) async {
    final ScrollController controller = ScrollController();
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView(
          controller: controller,
          children: List<Widget>.generate(
            80,
            (int i) => Text('$i', textDirection: TextDirection.ltr),
          ),
        ),
      ),
    );

    expectNoAnimation();

    final double halfTolerance =
        controller.position.physics.toleranceFor(controller.position).distance / 2;
    expect(halfTolerance, isNonZero);
    final double targetPosition = controller.position.pixels + halfTolerance;
    controller.position.animateTo(
      targetPosition,
      duration: const Duration(seconds: 10),
      curve: Curves.linear,
    );

    expectNoAnimation();
    expect(controller.position.pixels, targetPosition);
  });

  testWidgets('Animates if going to a position outside of tolerance', (WidgetTester tester) async {
    final ScrollController controller = ScrollController();
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: ListView(
          controller: controller,
          children: List<Widget>.generate(
            80,
            (int i) => Text('$i', textDirection: TextDirection.ltr),
          ),
        ),
      ),
    );

    expectNoAnimation();

    final double doubleTolerance =
        controller.position.physics.toleranceFor(controller.position).distance * 2;
    expect(doubleTolerance, isNonZero);
    final double targetPosition = controller.position.pixels + doubleTolerance;
    controller.position.animateTo(
      targetPosition,
      duration: const Duration(seconds: 10),
      curve: Curves.linear,
    );

    expect(
      SchedulerBinding.instance.transientCallbackCount,
      equals(1),
      reason: 'Expected an animation.',
    );
  });

  testWidgets('HoldActivity can interrupt ScrollPosition.animateTo', (WidgetTester tester) async {
    const double animationExtent = 100.0;
    const double dragExtent = 30.0;
    final ScrollController controller = ScrollController();
    addTearDown(controller.dispose);
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: NotificationListener<ScrollNotification>(
          onNotification: (ScrollNotification notification) {
            controller.position.animateTo(
              animationExtent,
              duration: const Duration(seconds: 1),
              curve: Curves.linear,
            );
            return true;
          },
          child: ListView(
            controller: controller,
            dragStartBehavior: DragStartBehavior.down,
            children: List<Widget>.generate(
              80,
              (int i) => Text('$i', textDirection: TextDirection.ltr),
            ),
          ),
        ),
      ),
    );

    expectNoAnimation();

    // Drag to initiate the scroll animation.
    await tester.drag(find.byType(Scrollable), const Offset(0.0, 1.0));
    await tester.pump();

    // Pump to halfway through the animation.
    await tester.pump(const Duration(milliseconds: 500));
    expect(controller.position.pixels, animationExtent / 2);

    // Interrupt the scroll animation.
    final TestGesture gesture = await tester.startGesture(
      tester.getCenter(find.byType(Scrollable)),
    );
    await gesture.moveBy(const Offset(0.0, dragExtent));
    await gesture.up();

    await tester.pump(const Duration(milliseconds: 500));

    // The drag stops the animation, and the drag extent is respected.
    expect(controller.position.pixels, (animationExtent / 2) - dragExtent);
  });
}

void expectNoAnimation() {
  expect(
    SchedulerBinding.instance.transientCallbackCount,
    equals(0),
    reason: 'Expected no animation.',
  );
}
