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

import '../scheduler/scheduler_tester.dart';

void main() {
  setUp(() {
    WidgetsFlutterBinding.ensureInitialized();
    WidgetsBinding.instance
        ..resetEpoch()
        ..platformDispatcher.onBeginFrame = null
        ..platformDispatcher.onDrawFrame = null;
  });

  test('Can set value during status callback', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );
    bool didComplete = false;
    bool didDismiss = false;
    controller.addStatusListener((AnimationStatus status) {
      if (status == AnimationStatus.completed) {
        didComplete = true;
        controller.value = 0.0;
        controller.forward();
      } else if (status == AnimationStatus.dismissed) {
        didDismiss = true;
        controller.value = 0.0;
        controller.forward();
      }
    });

    controller.forward();
    expect(didComplete, isFalse);
    expect(didDismiss, isFalse);
    tick(const Duration(seconds: 1));
    expect(didComplete, isFalse);
    expect(didDismiss, isFalse);
    tick(const Duration(seconds: 2));
    expect(didComplete, isTrue);
    expect(didDismiss, isTrue);

    controller.stop();
    controller.dispose();
  });

  test('Receives status callbacks for forward and reverse', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );
    final List<double> valueLog = <double>[];
    final List<AnimationStatus> log = <AnimationStatus>[];
    controller
      ..addStatusListener(log.add)
      ..addListener(() {
        valueLog.add(controller.value);
      });

    expect(log, equals(<AnimationStatus>[]));
    expect(valueLog, equals(<AnimationStatus>[]));

    controller.forward();

    expect(log, equals(<AnimationStatus>[AnimationStatus.forward]));
    expect(valueLog, equals(<AnimationStatus>[]));

    controller.reverse();

    expect(log, equals(<AnimationStatus>[AnimationStatus.forward, AnimationStatus.dismissed]));
    expect(valueLog, equals(<AnimationStatus>[]));

    controller.reverse();

    expect(log, equals(<AnimationStatus>[AnimationStatus.forward, AnimationStatus.dismissed]));
    expect(valueLog, equals(<AnimationStatus>[]));

    log.clear();
    controller.forward();

    expect(log, equals(<AnimationStatus>[AnimationStatus.forward]));
    expect(valueLog, equals(<AnimationStatus>[]));

    controller.forward();

    expect(log, equals(<AnimationStatus>[AnimationStatus.forward]));
    expect(valueLog, equals(<AnimationStatus>[]));

    controller.reverse();
    log.clear();

    tick(const Duration(seconds: 10));
    expect(log, equals(<AnimationStatus>[]));
    expect(valueLog, equals(<AnimationStatus>[]));
    tick(const Duration(seconds: 20));
    expect(log, equals(<AnimationStatus>[]));
    expect(valueLog, equals(<AnimationStatus>[]));
    tick(const Duration(seconds: 30));
    expect(log, equals(<AnimationStatus>[]));
    expect(valueLog, equals(<AnimationStatus>[]));
    tick(const Duration(seconds: 40));
    expect(log, equals(<AnimationStatus>[]));
    expect(valueLog, equals(<AnimationStatus>[]));

    controller.stop();
    controller.dispose();
  });

  test('Forward and reverse from values', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );
    final List<double> valueLog = <double>[];
    final List<AnimationStatus> statusLog = <AnimationStatus>[];
    controller
      ..addStatusListener(statusLog.add)
      ..addListener(() {
        valueLog.add(controller.value);
      });

    controller.reverse(from: 0.2);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse ]));
    expect(valueLog, equals(<double>[ 0.2 ]));
    expect(controller.value, equals(0.2));
    statusLog.clear();
    valueLog.clear();

    controller.forward(from: 0.0);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.dismissed, AnimationStatus.forward ]));
    expect(valueLog, equals(<double>[ 0.0 ]));
    expect(controller.value, equals(0.0));
    controller.dispose();
  });

  test('Forward and reverse with different durations', () {
    AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      reverseDuration: const Duration(milliseconds: 50),
      vsync: const TestVSync(),
    );

    controller.forward();
    tick(const Duration(milliseconds: 10));
    tick(const Duration(milliseconds: 30));
    expect(controller.value, moreOrLessEquals(0.2));
    tick(const Duration(milliseconds: 60));
    expect(controller.value, moreOrLessEquals(0.5));
    tick(const Duration(milliseconds: 90));
    expect(controller.value, moreOrLessEquals(0.8));
    tick(const Duration(milliseconds: 120));
    expect(controller.value, moreOrLessEquals(1.0));
    controller.stop();

    controller.reverse();
    tick(const Duration(milliseconds: 210));
    tick(const Duration(milliseconds: 220));
    expect(controller.value, moreOrLessEquals(0.8));
    tick(const Duration(milliseconds: 230));
    expect(controller.value, moreOrLessEquals(0.6));
    tick(const Duration(milliseconds: 240));
    expect(controller.value, moreOrLessEquals(0.4));
    tick(const Duration(milliseconds: 260));
    expect(controller.value, moreOrLessEquals(0.0));
    controller.stop();

    // Swap which duration is longer.
    controller = AnimationController(
      duration: const Duration(milliseconds: 50),
      reverseDuration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );

    controller.forward();
    tick(const Duration(milliseconds: 10));
    tick(const Duration(milliseconds: 30));
    expect(controller.value, moreOrLessEquals(0.4));
    tick(const Duration(milliseconds: 60));
    expect(controller.value, moreOrLessEquals(1.0));
    tick(const Duration(milliseconds: 90));
    expect(controller.value, moreOrLessEquals(1.0));
    controller.stop();

    controller.reverse();
    tick(const Duration(milliseconds: 210));
    tick(const Duration(milliseconds: 220));
    expect(controller.value, moreOrLessEquals(0.9));
    tick(const Duration(milliseconds: 230));
    expect(controller.value, moreOrLessEquals(0.8));
    tick(const Duration(milliseconds: 240));
    expect(controller.value, moreOrLessEquals(0.7));
    tick(const Duration(milliseconds: 260));
    expect(controller.value, moreOrLessEquals(0.5));
    tick(const Duration(milliseconds: 310));
    expect(controller.value, moreOrLessEquals(0.0));
    controller.stop();
    controller.dispose();
  });

  test('Forward only from value', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );
    final List<double> valueLog = <double>[];
    final List<AnimationStatus> statusLog = <AnimationStatus>[];
    controller
      ..addStatusListener(statusLog.add)
      ..addListener(() {
        valueLog.add(controller.value);
      });

    controller.forward(from: 0.2);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward ]));
    expect(valueLog, equals(<double>[ 0.2 ]));
    expect(controller.value, equals(0.2));
    controller.dispose();
  });

  test('Can fling to upper and lower bounds', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );

    controller.fling();
    tick(const Duration(seconds: 1));
    tick(const Duration(seconds: 2));
    expect(controller.value, 1.0);
    controller.stop();

    final AnimationController largeRangeController = AnimationController(
      duration: const Duration(milliseconds: 100),
      lowerBound: -30.0,
      upperBound: 45.0,
      vsync: const TestVSync(),
    );

    largeRangeController.fling();
    tick(const Duration(seconds: 3));
    tick(const Duration(seconds: 4));
    expect(largeRangeController.value, 45.0);
    largeRangeController.fling(velocity: -1.0);
    tick(const Duration(seconds: 5));
    tick(const Duration(seconds: 6));
    expect(largeRangeController.value, -30.0);
    largeRangeController.stop();
    controller.dispose();
    largeRangeController.dispose();
  });

  test('Custom springDescription can be applied', () {
    final AnimationController controller = AnimationController(
      vsync: const TestVSync(),
    );
    final AnimationController customSpringController = AnimationController(
      vsync: const TestVSync(),
    );

    controller.fling();
    // Will produce longer and smoother animation than the default.
    customSpringController.fling(
      springDescription: SpringDescription.withDampingRatio(
        mass: 0.01,
        stiffness: 10.0,
        ratio: 2.0,
      ),
    );
    tick(Duration.zero);
    tick(const Duration(milliseconds: 50));

    expect(customSpringController.value < controller.value, true);
    controller.dispose();
    customSpringController.dispose();
  });

  test('lastElapsedDuration control test', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );
    controller.forward();
    tick(const Duration(milliseconds: 20));
    tick(const Duration(milliseconds: 30));
    tick(const Duration(milliseconds: 40));
    expect(controller.lastElapsedDuration, equals(const Duration(milliseconds: 20)));
    controller.stop();
    controller.dispose();
  });

  test('toString control test', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );
    expect(controller, hasOneLineDescription);
    controller.forward();
    tick(const Duration(milliseconds: 10));
    tick(const Duration(milliseconds: 20));
    expect(controller, hasOneLineDescription);
    tick(const Duration(milliseconds: 30));
    expect(controller, hasOneLineDescription);
    controller.reverse();
    tick(const Duration(milliseconds: 40));
    tick(const Duration(milliseconds: 50));
    expect(controller, hasOneLineDescription);
    controller.stop();
    controller.dispose();
  });

  test('velocity test - linear', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 1000),
      vsync: const TestVSync(),
    );

    // mid-flight
    controller.forward();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 500));
    expect(controller.velocity, inInclusiveRange(0.9, 1.1));

    // edges
    controller.forward();
    expect(controller.velocity, inInclusiveRange(0.4, 0.6));
    tick(Duration.zero);
    expect(controller.velocity, inInclusiveRange(0.4, 0.6));
    tick(const Duration(milliseconds: 5));
    expect(controller.velocity, inInclusiveRange(0.9, 1.1));

    controller.forward(from: 0.5);
    expect(controller.velocity, inInclusiveRange(0.4, 0.6));
    tick(Duration.zero);
    expect(controller.velocity, inInclusiveRange(0.4, 0.6));
    tick(const Duration(milliseconds: 5));
    expect(controller.velocity, inInclusiveRange(0.9, 1.1));

    // stopped
    controller.forward(from: 1.0);
    expect(controller.velocity, 0.0);
    tick(Duration.zero);
    expect(controller.velocity, 0.0);
    tick(const Duration(milliseconds: 500));
    expect(controller.velocity, 0.0);

    controller.forward();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 1000));
    expect(controller.velocity, 0.0);

    controller.stop();
    controller.dispose();
  });

  test('Disposed AnimationController toString works', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );
    controller.dispose();
    expect(controller, hasOneLineDescription);
  });

  test('AnimationController error handling', () {
    final AnimationController controller = AnimationController(
      vsync: const TestVSync(),
    );

    expect(controller.forward, throwsFlutterError);
    expect(controller.reverse, throwsFlutterError);
    expect(() { controller.animateTo(0.5); }, throwsFlutterError);
    expect(controller.repeat, throwsFlutterError);

    controller.dispose();
    FlutterError? result;
    try {
      controller.dispose();
    } on FlutterError catch (e) {
      result = e;
    }
    expect(result, isNotNull);
    expect(
      result!.toStringDeep(),
      equalsIgnoringHashCodes(
        'FlutterError\n'
        '   AnimationController.dispose() called more than once.\n'
        '   A given AnimationController cannot be disposed more than once.\n'
        '   The following AnimationController object was disposed multiple\n'
        '   times:\n'
        '     AnimationController#00000(⏮ 0.000; paused; DISPOSED)\n',
      ),
    );
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    result.debugFillProperties(builder);
    final DiagnosticsNode controllerProperty = builder.properties.last;
    expect(controllerProperty.name, 'The following AnimationController object was disposed multiple times');
    expect(controllerProperty.value, controller);
  });

  test('AnimationController repeat() throws if period is not specified', () {
    final AnimationController controller = AnimationController(
      vsync: const TestVSync(),
    );
    expect(() { controller.repeat(); }, throwsFlutterError);
    expect(() { controller.repeat(); }, throwsFlutterError);
    controller.dispose();
  });

  test('Do not animate if already at target', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];

    final AnimationController controller = AnimationController(
      value: 0.5,
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, equals(0.5));
    controller.animateTo(0.5, duration: const Duration(milliseconds: 100));
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.completed ]));
    expect(controller.value, equals(0.5));
    controller.dispose();
  });

  test('Do not animate to upperBound if already at upperBound', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];

    final AnimationController controller = AnimationController(
      value: 1.0,
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, equals(1.0));
    controller.animateTo(1.0, duration: const Duration(milliseconds: 100));
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.completed ]));
    expect(controller.value, equals(1.0));
    controller.dispose();
  });

  test('Do not animate to lowerBound if already at lowerBound', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];

    final AnimationController controller = AnimationController(
      value: 0.0,
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, equals(0.0));
    controller.animateTo(0.0, duration: const Duration(milliseconds: 100));
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.completed ]));
    expect(controller.value, equals(0.0));
    controller.dispose();
  });

  test('Do not animate if already at target mid-flight (forward)', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      value: 0.0,
      duration: const Duration(milliseconds: 1000),
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, equals(0.0));

    controller.forward();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 500));
    expect(controller.value, inInclusiveRange(0.4, 0.6));
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward ]));

    final double currentValue = controller.value;
    controller.animateTo(currentValue, duration: const Duration(milliseconds: 100));
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ]));
    expect(controller.value, currentValue);
    controller.dispose();
  });

  test('Do not animate if already at target mid-flight (reverse)', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      value: 1.0,
      duration: const Duration(milliseconds: 1000),
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, equals(1.0));

    controller.reverse();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 500));
    expect(controller.value, inInclusiveRange(0.4, 0.6));
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse ]));

    final double currentValue = controller.value;
    controller.animateTo(currentValue, duration: const Duration(milliseconds: 100));
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse, AnimationStatus.completed ]));
    expect(controller.value, currentValue);
    controller.dispose();
  });

  test('animateTo can deal with duration == Duration.zero', () {
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: const TestVSync(),
    );

    controller.forward(from: 0.2);
    expect(controller.value, 0.2);
    controller.animateTo(1.0, duration: Duration.zero);
    expect(SchedulerBinding.instance.transientCallbackCount, equals(0), reason: 'Expected no animation.');
    expect(controller.value, 1.0);
    controller.dispose();
  });

  test('resetting animation works at all phases', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      value: 0.0,
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);

    controller.reset();

    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);

    statusLog.clear();
    controller.forward();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 50));
    expect(controller.status, AnimationStatus.forward);
    controller.reset();

    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.dismissed ]));

    controller.value = 1.0;
    statusLog.clear();
    controller.reverse();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 50));
    expect(controller.status, AnimationStatus.reverse);
    controller.reset();

    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse, AnimationStatus.dismissed ]));

    statusLog.clear();
    controller.forward();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.status, AnimationStatus.completed);
    controller.reset();

    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed, AnimationStatus.dismissed ]));
    controller.dispose();
  });

  test('setting value directly sets correct status', () {
    final AnimationController controller = AnimationController(
      value: 0.0,
      vsync: const TestVSync(),
    );

    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);

    controller.value = 0.5;
    expect(controller.value, 0.5);
    expect(controller.status, AnimationStatus.forward);

    controller.value = 1.0;
    expect(controller.value, 1.0);
    expect(controller.status, AnimationStatus.completed);

    controller.value = 0.5;
    expect(controller.value, 0.5);
    expect(controller.status, AnimationStatus.forward);

    controller.value = 0.0;
    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);
    controller.dispose();
  });

  test('animateTo sets correct status', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      value: 0.0,
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);

    // Animate from 0.0 to 0.5
    controller.animateTo(0.5);
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.value, 0.5);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ]));
    statusLog.clear();

    // Animate from 0.5 to 1.0
    controller.animateTo(1.0);
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.value, 1.0);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ]));
    statusLog.clear();

    // Animate from 1.0 to 0.5
    controller.animateTo(0.5);
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.value, 0.5);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ]));
    statusLog.clear();

    // Animate from 0.5 to 1.0
    controller.animateTo(0.0);
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.value, 0.0);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ]));
    statusLog.clear();
    controller.dispose();
  });

  test('after a reverse call animateTo sets correct status', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      value: 1.0,
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, 1.0);
    expect(controller.status, AnimationStatus.completed);

    controller.reverse();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.value, 0.0);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.reverse, AnimationStatus.dismissed ]));
    statusLog.clear();

    controller.animateTo(0.5);
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.value, 0.5);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ]));
    statusLog.clear();
    controller.dispose();
  });

  test('after a forward call animateTo sets correct status', () {
    final List<AnimationStatus> statusLog = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      value: 0.0,
      vsync: const TestVSync(),
    )..addStatusListener(statusLog.add);

    expect(controller.value, 0.0);
    expect(controller.status, AnimationStatus.dismissed);

    controller.forward();
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.value, 1.0);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ]));
    statusLog.clear();

    controller.animateTo(0.5);
    tick(Duration.zero);
    tick(const Duration(milliseconds: 150));
    expect(controller.value, 0.5);
    expect(statusLog, equals(<AnimationStatus>[ AnimationStatus.forward, AnimationStatus.completed ]));
    statusLog.clear();
    controller.dispose();
  });

  test(
    'calling repeat with reverse set to true makes the animation alternate '
    'between lowerBound and upperBound values on each repeat',
    () {
      final AnimationController controller = AnimationController(
        duration: const Duration(milliseconds: 100),
        value: 0.0,
        vsync: const TestVSync(),
      );

      expect(controller.value, 0.0);

      controller.repeat(reverse: true);
      tick(Duration.zero);
      tick(const Duration(milliseconds: 25));
      expect(controller.value, 0.25);

      tick(Duration.zero);
      tick(const Duration(milliseconds: 125));
      expect(controller.value, 0.75);

      controller.reset();
      controller.value = 1.0;
      expect(controller.value, 1.0);

      controller.repeat(reverse: true);
      tick(Duration.zero);
      tick(const Duration(milliseconds: 25));
      expect(controller.value, 0.75);

      tick(Duration.zero);
      tick(const Duration(milliseconds: 125));
      expect(controller.value, 0.25);

      controller.reset();
      controller.value = 0.5;
      expect(controller.value, 0.5);

      controller.repeat(reverse: true);
      tick(Duration.zero);
      tick(const Duration(milliseconds: 50));
      expect(controller.value, 1.0);

      tick(Duration.zero);
      tick(const Duration(milliseconds: 150));
      expect(controller.value, 0.0);
      controller.dispose();
    },
  );

  test(
    'calling repeat with specified min and max values makes the animation '
    'alternate between min and max values on each repeat',
    () {
      final AnimationController controller = AnimationController(
        duration: const Duration(milliseconds: 100),
        value: 0.0,
        vsync: const TestVSync(),
      );

      expect(controller.value, 0.0);

      controller.repeat(reverse: true, min: 0.5, max: 1.0);
      tick(Duration.zero);
      tick(const Duration(milliseconds: 50));
      expect(controller.value, 0.75);

      tick(Duration.zero);
      tick(const Duration(milliseconds: 100));
      expect(controller.value, 1.00);

      tick(Duration.zero);
      tick(const Duration(milliseconds: 200));
      expect(controller.value, 0.5);

      controller.reset();
      controller.value = 0.0;
      expect(controller.value, 0.0);

      controller.repeat(reverse: true, min: 1.0, max: 1.0);
      tick(Duration.zero);
      tick(const Duration(milliseconds: 25));
      expect(controller.value, 1.0);

      tick(Duration.zero);
      tick(const Duration(milliseconds: 125));
      expect(controller.value, 1.0);
      controller.dispose();
    },
  );

  group('AnimationBehavior', () {
    test('Default values for constructor', () {
      final AnimationController controller = AnimationController(vsync: const TestVSync());
      expect(controller.animationBehavior, AnimationBehavior.normal);

      final AnimationController repeating = AnimationController.unbounded(vsync: const TestVSync());
      expect(repeating.animationBehavior, AnimationBehavior.preserve);
      controller.dispose();
      repeating.dispose();
    });

    test('AnimationBehavior.preserve runs at normal speed when animatingTo', () {
      debugSemanticsDisableAnimations = true;
      final AnimationController controller = AnimationController(
        vsync: const TestVSync(),
        animationBehavior: AnimationBehavior.preserve,
      );

      expect(controller.value, 0.0);
      expect(controller.status, AnimationStatus.dismissed);

      controller.animateTo(1.0, duration: const Duration(milliseconds: 100));
      tick(Duration.zero);
      tick(const Duration(milliseconds: 50));

      expect(controller.value, 0.5);
      expect(controller.status, AnimationStatus.forward);

      tick(Duration.zero);
      tick(const Duration(milliseconds: 150));

      expect(controller.value, 1.0);
      expect(controller.status, AnimationStatus.completed);
      debugSemanticsDisableAnimations = false;
      controller.dispose();
    });

    test('AnimationBehavior.normal runs at 20x speed when animatingTo', () {
      debugSemanticsDisableAnimations = true;
      final AnimationController controller = AnimationController(
        vsync: const TestVSync(),
      );

      expect(controller.value, 0.0);
      expect(controller.status, AnimationStatus.dismissed);

      controller.animateTo(1.0, duration: const Duration(milliseconds: 100));
      tick(Duration.zero);
      tick(const Duration(microseconds: 2500));

      expect(controller.value, 0.5);
      expect(controller.status, AnimationStatus.forward);

      tick(Duration.zero);
      tick(const Duration(milliseconds: 5, microseconds: 1000));

      expect(controller.value, 1.0);
      expect(controller.status, AnimationStatus.completed);
      debugSemanticsDisableAnimations = null;
      controller.dispose();
    });

    test('AnimationBehavior.normal runs "faster" than AnimationBehavior.preserve', () {
      debugSemanticsDisableAnimations = true;
      final AnimationController controller = AnimationController(
        vsync: const TestVSync(),
      );
      final AnimationController fastController = AnimationController(
        vsync: const TestVSync(),
      );

      controller.fling(animationBehavior: AnimationBehavior.preserve);
      fastController.fling(animationBehavior: AnimationBehavior.normal);
      tick(Duration.zero);
      tick(const Duration(milliseconds: 50));

      // We don't assert a specific faction that normal animation.
      expect(controller.value < fastController.value, true);
      debugSemanticsDisableAnimations = null;
      controller.dispose();
      fastController.dispose();
    });
  });

  test('AnimationController methods assert _ticker is not null', () {
    final AnimationController controller = AnimationController(
      vsync: const TestVSync(),
    );

    controller.dispose();

    expect(() => controller.animateBack(0), throwsAssertionError);
    expect(() => controller.animateTo(0), throwsAssertionError);
    expect(() => controller.animateWith(GravitySimulation(0, 0, 0, 0)), throwsAssertionError);
    expect(() => controller.stop(), throwsAssertionError);
    expect(() => controller.forward(), throwsAssertionError);
    expect(() => controller.reverse(), throwsAssertionError);
  });

  test('Simulations run forward', () {
    final List<AnimationStatus> statuses = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      vsync: const TestVSync(),
      duration: const Duration(seconds: 1),
    )..addStatusListener((AnimationStatus status) {
      statuses.add(status);
    });

    controller.animateWith(TestSimulation());
    tick(Duration.zero);
    tick(const Duration(seconds: 2));
    expect(statuses, <AnimationStatus>[AnimationStatus.forward]);
    controller.dispose();
  });

  test('Simulations run forward even after a reverse run', () {
    final List<AnimationStatus> statuses = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      vsync: const TestVSync(),
      duration: const Duration(seconds: 1),
    )..addStatusListener((AnimationStatus status) {
      statuses.add(status);
    });
    controller.reverse(from: 1.0);
    tick(Duration.zero);
    tick(const Duration(seconds: 2));
    expect(statuses, <AnimationStatus>[AnimationStatus.completed, AnimationStatus.reverse, AnimationStatus.dismissed]);
    statuses.clear();

    controller.animateWith(TestSimulation());
    tick(Duration.zero);
    tick(const Duration(seconds: 2));
    expect(statuses, <AnimationStatus>[AnimationStatus.forward]);
    controller.dispose();
  });

  test('Repeating animation with reverse: true report as forward and reverse', () {
    final List<AnimationStatus> statuses = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      vsync: const TestVSync(),
      duration: const Duration(seconds: 1),
    )..addStatusListener((AnimationStatus status) {
      statuses.add(status);
    });

    controller.repeat(reverse: true);
    tick(Duration.zero);
    tick(const Duration(milliseconds: 999));
    expect(statuses, <AnimationStatus>[AnimationStatus.forward]);
    statuses.clear();
    tick(const Duration(seconds: 1));
    expect(statuses, <AnimationStatus>[AnimationStatus.reverse]);
    controller.dispose();
  });

  test('AnimateBack can runs successfully with just "reverseDuration" property set', () {
    final List<AnimationStatus> statuses = <AnimationStatus>[];
    final AnimationController controller = AnimationController(
      reverseDuration: const Duration(seconds: 2),
      vsync: const TestVSync(),
    )..addStatusListener((AnimationStatus status) {
      statuses.add(status);
    });

    controller.animateBack(0.8);

    expect(statuses, <AnimationStatus>[AnimationStatus.reverse]);
    statuses.clear();
    tick(Duration.zero);
    tick(const Duration(seconds: 2));
    expect(statuses, <AnimationStatus>[AnimationStatus.dismissed]);

    controller.dispose();
  });

  group('AnimationController "duration" error test', () {
    test('AnimationController forward() will throw an error if there is no default duration', () {
      final AnimationController controller = AnimationController(
        vsync: const TestVSync(),
      );

      late FlutterError error;
      try {
        controller.forward();
      } on FlutterError catch (e) {
        error = e;
      }

      expect(error, isNotNull);
      expect(
        error.toStringDeep(),
        'FlutterError\n'
        '   AnimationController.forward() called with no default duration.\n'
        '   The "duration" property should be set, either in the constructor\n'
        '   or later, before calling the forward() function.\n',
      );

      controller.dispose();
    });

    test(
      'AnimationController animateTo() will throw an error if there is no explicit duration '
      'and default duration',
      () {
        final AnimationController controller = AnimationController(
          vsync: const TestVSync(),
        );

        late FlutterError error;
        try {
          controller.animateTo(0.8);
        } on FlutterError catch (e) {
          error = e;
        }

        expect(error, isNotNull);
        expect(
          error.toStringDeep(),
          'FlutterError\n'
          '   AnimationController.animateTo() called with no explicit duration\n'
          '   and no default duration.\n'
          '   Either the "duration" argument to the animateTo() method should\n'
          '   be provided, or the "duration" property should be set, either in\n'
          '   the constructor or later, before calling the animateTo()\n'
          '   function.\n',
        );

        controller.dispose();
      },
    );

    test('AnimationController reverse() will throw an error if there is no default duration or reverseDuration', () {
      final AnimationController controller = AnimationController(
        vsync: const TestVSync(),
      );

      late FlutterError error;
      try {
        controller.reverse();
      } on FlutterError catch (e) {
        error = e;
      }

      expect(error, isNotNull);
      expect(
        error.toStringDeep(),
        'FlutterError\n'
        '   AnimationController.reverse() called with no default duration or\n'
        '   reverseDuration.\n'
        '   The "duration" or "reverseDuration" property should be set,\n'
        '   either in the constructor or later, before calling the reverse()\n'
        '   function.\n',
      );

      controller.dispose();
    });

    test(
      'AnimationController animateBack() will throw an error if there is no explicit duration and '
      'no default duration or reverseDuration',
      () {
        final AnimationController controller = AnimationController(
          vsync: const TestVSync(),
        );

        late FlutterError error;
        try {
          controller.animateBack(0.8);
        } on FlutterError catch (e) {
          error = e;
        }

        expect(error, isNotNull);
        expect(
          error.toStringDeep(),
          'FlutterError\n'
          '   AnimationController.animateBack() called with no explicit\n'
          '   duration and no default duration or reverseDuration.\n'
          '   Either the "duration" argument to the animateBack() method should\n'
          '   be provided, or the "duration" or "reverseDuration" property\n'
          '   should be set, either in the constructor or later, before calling\n'
          '   the animateBack() function.\n',
        );

        controller.dispose();
      },
    );
  });

}

class TestSimulation extends Simulation {
  @override
  double dx(double time) => time;

  @override
  bool isDone(double time) => false;

  @override
  double x(double time) => time;
}
