// 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/widgets.dart';

import 'package:flutter_test/flutter_test.dart';

void main() {
  test('test_friction', () {
    final FrictionSimulation friction = FrictionSimulation(0.3, 100.0, 400.0);

    friction.tolerance = const Tolerance(velocity: 1.0);

    expect(friction.isDone(0.0), false);
    expect(friction.x(0.0), 100);
    expect(friction.dx(0.0), 400.0);

    expect(friction.x(1.0) > 330 && friction.x(1.0) < 335, true);

    expect(friction.dx(1.0), 120.0);
    expect(friction.dx(2.0), 36.0);
    expect(friction.dx(3.0), moreOrLessEquals(10.8));
    expect(friction.dx(4.0) < 3.5, true);

    expect(friction.isDone(5.0), true);
    expect(friction.x(5.0) > 431 && friction.x(5.0) < 432, true);
  });

  test('test_friction_through', () {
    // Use a normal FrictionSimulation to generate start and end
    // velocity and positions with drag = 0.025.
    double startPosition = 10.0;
    double startVelocity = 600.0;
    FrictionSimulation f = FrictionSimulation(0.025, startPosition, startVelocity);
    double endPosition = f.x(1.0);
    double endVelocity = f.dx(1.0);
    expect(endPosition, greaterThan(startPosition));
    expect(endVelocity, lessThan(startVelocity));

    // Verify that the "through" FrictionSimulation ends up at
    // endPosition and endVelocity; implies that it computed the right
    // value for _drag.
    FrictionSimulation friction = FrictionSimulation.through(startPosition, endPosition, startVelocity, endVelocity);
    expect(friction.isDone(0.0), false);
    expect(friction.x(0.0), 10.0);
    expect(friction.dx(0.0), 600.0);

    expect(friction.isDone(1.0 + precisionErrorTolerance), true);
    expect(friction.x(1.0), moreOrLessEquals(endPosition));
    expect(friction.dx(1.0), moreOrLessEquals(endVelocity));

    // Same scenario as above except that the velocities are
    // are negative.
    startPosition = 1000.0;
    startVelocity = -500.0;
    f = FrictionSimulation(0.025, 1000.0, -500.0);
    endPosition = f.x(1.0);
    endVelocity = f.dx(1.0);
    expect(endPosition, lessThan(startPosition));
    expect(endVelocity, greaterThan(startVelocity));

    friction = FrictionSimulation.through(startPosition, endPosition, startVelocity, endVelocity);
    expect(friction.isDone(1.0 + precisionErrorTolerance), true);
    expect(friction.x(1.0), moreOrLessEquals(endPosition));
    expect(friction.dx(1.0), moreOrLessEquals(endVelocity));
  });

  test('BoundedFrictionSimulation control test', () {
    final BoundedFrictionSimulation friction = BoundedFrictionSimulation(0.3, 100.0, 400.0, 50.0, 150.0);

    friction.tolerance = const Tolerance(velocity: 1.0);

    expect(friction.isDone(0.0), false);
    expect(friction.x(0.0), 100);
    expect(friction.dx(0.0), 400.0);

    expect(friction.x(1.0), equals(150.0));

    expect(friction.isDone(1.0), true);
  });

  test('test_gravity', () {
    final GravitySimulation gravity = GravitySimulation(200.0, 100.0, 600.0, 0.0);

    expect(gravity.isDone(0.0), false);
    expect(gravity.x(0.0), 100.0);
    expect(gravity.dx(0.0), 0.0);

    // Starts at 100
    expect(gravity.x(0.25), 106.25);
    expect(gravity.x(0.50), 125);
    expect(gravity.x(0.75), 156.25);
    expect(gravity.x(1.00), 200);
    expect(gravity.x(1.25), 256.25);
    expect(gravity.x(1.50), 325);
    expect(gravity.x(1.75), 406.25);

    // Starts at 0.0
    expect(gravity.dx(0.25), 50.0);
    expect(gravity.dx(0.50), 100);
    expect(gravity.dx(0.75), 150.00);
    expect(gravity.dx(1.00), 200.0);
    expect(gravity.dx(1.25), 250.0);
    expect(gravity.dx(1.50), 300);
    expect(gravity.dx(1.75), 350);

    expect(gravity.isDone(2.5), true);
    expect(gravity.x(2.5), 725);
    expect(gravity.dx(2.5), 500.0);
  });

  test('spring_types', () {
    SpringSimulation crit = SpringSimulation(SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 100.0,
    ), 0.0, 300.0, 0.0);
    expect(crit.type, SpringType.criticallyDamped);

    crit = SpringSimulation(SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 100.0,
      ratio: 1.0,
    ), 0.0, 300.0, 0.0);
    expect(crit.type, SpringType.criticallyDamped);

    final SpringSimulation under = SpringSimulation(SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 100.0,
      ratio: 0.75,
    ), 0.0, 300.0, 0.0);
    expect(under.type, SpringType.underDamped);

    final SpringSimulation over = SpringSimulation(SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 100.0,
      ratio: 1.25,
    ), 0.0, 300.0, 0.0);
    expect(over.type, SpringType.overDamped);

    // Just so we don't forget how to create a desc without the ratio.
    final SpringSimulation other = SpringSimulation(const SpringDescription(
      mass: 1.0,
      stiffness: 100.0,
      damping: 20.0,
    ), 0.0, 20.0, 20.0);
    expect(other.type, SpringType.criticallyDamped);
  });

  test('crit_spring', () {
    final SpringSimulation crit = SpringSimulation(SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 100.0,
      ratio: 1.0,
    ), 0.0, 500.0, 0.0);

    crit.tolerance = const Tolerance(distance: 0.01, velocity: 0.01);

    expect(crit.type, SpringType.criticallyDamped);

    expect(crit.isDone(0.0), false);
    expect(crit.x(0.0), 0.0);
    expect(crit.dx(0.0), 5000.0);

    expect(crit.x(0.25).floor(), 458.0);
    expect(crit.x(0.50).floor(), 496.0);
    expect(crit.x(0.75).floor(), 499.0);

    expect(crit.dx(0.25).floor(), 410);
    expect(crit.dx(0.50).floor(), 33);
    expect(crit.dx(0.75).floor(), 2);

    expect(crit.isDone(1.50), true);
    expect(crit.x(1.5) > 499.0 && crit.x(1.5) < 501.0, true);
    expect(crit.dx(1.5) < 0.1, true /* basically within tolerance */);
  });

  test('overdamped_spring', () {
    final SpringSimulation over = SpringSimulation(SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 100.0,
      ratio: 1.25,
    ), 0.0, 500.0, 0.0);

    over.tolerance = const Tolerance(distance: 0.01, velocity: 0.01);

    expect(over.type, SpringType.overDamped);

    expect(over.isDone(0.0), false);
    expect(over.x(0.0), 0.0);

    expect(over.x(0.5).floor(), 445.0);
    expect(over.x(1.0).floor(), 495.0);
    expect(over.x(1.5).floor(), 499.0);

    expect(over.dx(0.5).floor(), 273.0);
    expect(over.dx(1.0).floor(), 22.0);
    expect(over.dx(1.5).floor(), 1.0);

    expect(over.isDone(3.0), true);
  });

  test('underdamped_spring', () {
    final SpringSimulation under = SpringSimulation(SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 100.0,
      ratio: 0.25,
    ), 0.0, 300.0, 0.0);
    expect(under.type, SpringType.underDamped);

    expect(under.isDone(0.0), false);

    // Overshot with negative velocity
    expect(under.x(1.0).floor(), 325);
    expect(under.dx(1.0).floor(), -65);

    expect(under.dx(6.0).floor(), 0.0);
    expect(under.x(6.0).floor(), 299);

    expect(under.isDone(6.0), true);
  });

  test('test_kinetic_scroll', () {
    final SpringDescription spring = SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 50.0,
      ratio: 0.5,
    );

    final BouncingScrollSimulation scroll = BouncingScrollSimulation(
      position: 100.0,
      velocity: 800.0,
      leadingExtent: 0.0,
      trailingExtent: 300.0,
      spring: spring,
    );
    scroll.tolerance = const Tolerance(velocity: 0.5, distance: 0.1);
    expect(scroll.isDone(0.0), false);
    expect(scroll.isDone(0.5), false); // switch from friction to spring
    expect(scroll.isDone(3.5), true);

    final BouncingScrollSimulation scroll2 = BouncingScrollSimulation(
      position: 100.0,
      velocity: -800.0,
      leadingExtent: 0.0,
      trailingExtent: 300.0,
      spring: spring,
    );
    scroll2.tolerance = const Tolerance(velocity: 0.5, distance: 0.1);
    expect(scroll2.isDone(0.0), false);
    expect(scroll2.isDone(0.5), false); // switch from friction to spring
    expect(scroll2.isDone(3.5), true);
  });

  test('scroll_with_inf_edge_ends', () {
    final SpringDescription spring = SpringDescription.withDampingRatio(
      mass: 1.0,
      stiffness: 50.0,
      ratio: 0.5,
    );

    final BouncingScrollSimulation scroll = BouncingScrollSimulation(
      position: 100.0,
      velocity: 400.0,
      leadingExtent: 0.0,
      trailingExtent: double.infinity,
      spring: spring,
    );
    scroll.tolerance = const Tolerance(velocity: 1.0);

    expect(scroll.isDone(0.0), false);
    expect(scroll.x(0.0), 100);
    expect(scroll.dx(0.0), 400.0);

    expect(scroll.x(1.0), moreOrLessEquals(272.0, epsilon: 1.0));

    expect(scroll.dx(1.0), moreOrLessEquals(54.0, epsilon: 1.0));
    expect(scroll.dx(2.0), moreOrLessEquals(7.0, epsilon: 1.0));
    expect(scroll.dx(3.0), lessThan(1.0));

    expect(scroll.isDone(5.0), true);
    expect(scroll.x(5.0), moreOrLessEquals(300.0, epsilon: 1.0));
  });

  test('over/under scroll spring', () {
    final SpringDescription spring = SpringDescription.withDampingRatio(mass: 1.0, stiffness: 170.0, ratio: 1.1);
    final BouncingScrollSimulation scroll = BouncingScrollSimulation(
      position: 500.0,
      velocity: -7500.0,
      leadingExtent: 0.0,
      trailingExtent: 1000.0,
      spring: spring,
    );
    scroll.tolerance = const Tolerance(velocity: 45.0, distance: 1.5);

    expect(scroll.isDone(0.0), false);
    expect(scroll.x(0.0), moreOrLessEquals(500.0));
    expect(scroll.dx(0.0), moreOrLessEquals(-7500.0));

    // Expect to reach 0.0 at about t=.07 at which point the simulation will
    // switch from friction to the spring
    expect(scroll.isDone(0.065), false);
    expect(scroll.x(0.065), moreOrLessEquals(42.0, epsilon: 1.0));
    expect(scroll.dx(0.065), moreOrLessEquals(-6584.0, epsilon: 1.0));

    // We've overscrolled (0.1 > 0.07). Trigger the underscroll
    // simulation, and reverse direction
    expect(scroll.isDone(0.1), false);
    expect(scroll.x(0.1), moreOrLessEquals(-123.0, epsilon: 1.0));
    expect(scroll.dx(0.1), moreOrLessEquals(-2613.0, epsilon: 1.0));

    // Headed back towards 0.0 and slowing down.
    expect(scroll.isDone(0.5), false);
    expect(scroll.x(0.5), moreOrLessEquals(-15.0, epsilon: 1.0));
    expect(scroll.dx(0.5), moreOrLessEquals(124.0, epsilon: 1.0));

    // Now jump back to the beginning, because we can.
    expect(scroll.isDone(0.0), false);
    expect(scroll.x(0.0), moreOrLessEquals(500.0));
    expect(scroll.dx(0.0), moreOrLessEquals(-7500.0));

    expect(scroll.isDone(2.0), true);
    expect(scroll.x(2.0), 0.0);
    expect(scroll.dx(2.0), moreOrLessEquals(0.0, epsilon: 1.0));
  });
}
