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

import 'gesture_tester.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  testGesture('A force press can be recognized', (GestureTester tester) {

    // Device specific constants that represent those from the iPhone X
    const double pressureMin = 0;
    const double pressureMax = 6.66;

    int started = 0;
    int peaked = 0;
    int updated = 0;
    int ended = 0;

    Offset? startGlobalPosition;

    void onStart(ForcePressDetails details) {
      startGlobalPosition = details.globalPosition;
      started += 1;
    }

    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();

    force.onStart = onStart;
    force.onPeak = (ForcePressDetails details) => peaked += 1;
    force.onUpdate = (ForcePressDetails details) => updated += 1;
    force.onEnd = (ForcePressDetails details) => ended += 1;

    const int pointerValue = 1;
    final TestPointer pointer = TestPointer();
    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
    pointer.setDownInfo(down, const Offset(10.0, 10.0));
    force.addPointer(down);
    tester.closeArena(pointerValue);

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    // Pressure fed into the test environment simulates the values received directly from the device.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have not hit the start pressure, so no events should be true.
    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.8, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have just hit the start pressure so just the start event should be triggered and one update call should have occurred.
    expect(started, 1);
    expect(peaked, 0);
    expect(updated, 1);
    expect(ended, 0);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 5.0, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressureMin: pressureMin, pressureMax: pressureMax));

    // We have exceeded the start pressure so update should be greater than 0.
    expect(started, 1);
    expect(updated, 5);
    expect(peaked, 0);
    expect(ended, 0);
    expect(startGlobalPosition, const Offset(10.0, 10.0));

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 6.0, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have exceeded the peak pressure so peak pressure should be true.
    expect(started, 1);
    expect(updated, 6);
    expect(peaked, 1);
    expect(ended, 0);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 5.0, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressureMin: pressureMin, pressureMax: pressureMax));

    // Update is still called.
    expect(started, 1);
    expect(updated, 10);
    expect(peaked, 1);
    expect(ended, 0);

    tester.route(pointer.up());

    // We have ended the gesture so ended should be true.
    expect(started, 1);
    expect(updated, 10);
    expect(peaked, 1);
    expect(ended, 1);
  });

  testGesture('Invalid pressure ranges capabilities are not recognized', (GestureTester tester) {
    void testGestureWithMaxPressure(double pressureMax) {
      int started = 0;
      int peaked = 0;
      int updated = 0;
      int ended = 0;

      final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();

      force.onStart = (ForcePressDetails details) => started += 1;
      force.onPeak = (ForcePressDetails details) => peaked += 1;
      force.onUpdate = (ForcePressDetails details) => updated += 1;
      force.onEnd = (ForcePressDetails details) => ended += 1;

      const int pointerValue = 1;
      final TestPointer pointer = TestPointer();
      final PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: const Offset(10.0, 10.0), pressure: 0, pressureMin: 0, pressureMax: pressureMax);
      pointer.setDownInfo(down, const Offset(10.0, 10.0));
      force.addPointer(down);
      tester.closeArena(pointerValue);

      expect(started, 0);
      expect(peaked, 0);
      expect(updated, 0);
      expect(ended, 0);

      // Pressure fed into the test environment simulates the values received directly from the device.
      tester.route(PointerMoveEvent(pointer: pointerValue, position: const Offset(10.0, 10.0), pressure: 10, pressureMin: 0, pressureMax: pressureMax));

      // Regardless of current pressure, this recognizer shouldn't participate or
      // trigger any callbacks.
      expect(started, 0);
      expect(peaked, 0);
      expect(updated, 0);
      expect(ended, 0);

      tester.route(pointer.up());

      // There should still be no callbacks.
      expect(started, 0);
      expect(updated, 0);
      expect(peaked, 0);
      expect(ended, 0);
    }

    testGestureWithMaxPressure(0);
    testGestureWithMaxPressure(1);
    testGestureWithMaxPressure(-1);
    testGestureWithMaxPressure(0.5);
  });

  testGesture('If minimum pressure is not reached, start and end callbacks are not called', (GestureTester tester) {
    // Device specific constants that represent those from the iPhone X
    const double pressureMin = 0;
    const double pressureMax = 6.66;

    int started = 0;
    int peaked = 0;
    int updated = 0;
    int ended = 0;

    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();

    force.onStart = (_) => started += 1;
    force.onPeak = (_) => peaked += 1;
    force.onUpdate = (_) => updated += 1;
    force.onEnd = (_) => ended += 1;

    const int pointerValue = 1;
    final TestPointer pointer = TestPointer();
    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
    pointer.setDownInfo(down, const Offset(10.0, 10.0));
    force.addPointer(down);
    tester.closeArena(1);

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    // Pressure fed into the test environment simulates the values received directly from the device.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have not hit the start pressure, so no events should be true.
    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    tester.route(pointer.up());

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
  });

  testGesture('Should recognize drag and not force touch if there is a drag recognizer', (GestureTester tester) {
    final PanGestureRecognizer drag = PanGestureRecognizer();

    // Device specific constants that represent those from the iPhone X
    const double pressureMin = 0;
    const double pressureMax = 6.66;

    int started = 0;
    int peaked = 0;
    int updated = 0;
    int ended = 0;

    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();

    force.onStart = (_) => started += 1;
    force.onPeak = (_) => peaked += 1;
    force.onUpdate = (_) => updated += 1;
    force.onEnd = (_) => ended += 1;

    int didStartPan = 0;
    drag.onStart = (_) => didStartPan += 1;

    const int pointerValue = 1;
    final TestPointer pointer = TestPointer();
    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressureMin: pressureMin, pressureMax: pressureMax);
    pointer.setDownInfo(down, const Offset(10.0, 10.0));
    force.addPointer(down);
    drag.addPointer(down);
    tester.closeArena(1);

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
    expect(didStartPan, 0);

    tester.route(pointer.move(const Offset(30.0, 30.0))); // moved 20 horizontally and 20 vertically which is 28 total

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
    expect(didStartPan, 1);

    // Pressure fed into the test environment simulates the values received directly from the device.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have not hit the start pressure, so no events should be true.
    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
    expect(didStartPan, 1);

    // We don't expect any events from the force press recognizer.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
    expect(didStartPan, 1);

    tester.route(pointer.up());

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
    expect(didStartPan, 1);
  });

  testGesture('Should not call ended on pointer up if the gesture was never accepted', (GestureTester tester) {
    final PanGestureRecognizer drag = PanGestureRecognizer();

    // Device specific constants that represent those from the iPhone X
    const double pressureMin = 0;
    const double pressureMax = 6.66;

    int started = 0;
    int peaked = 0;
    int updated = 0;
    int ended = 0;

    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();

    force.onStart = (_) => started += 1;
    force.onPeak = (_) => peaked += 1;
    force.onUpdate = (_) => updated += 1;
    force.onEnd = (_) => ended += 1;

    int didStartPan = 0;
    drag.onStart = (_) => didStartPan += 1;

    const int pointerValue = 1;
    final TestPointer pointer = TestPointer();
    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressureMin: pressureMin, pressureMax: pressureMax);
    pointer.setDownInfo(down, const Offset(10.0, 10.0));
    force.addPointer(down);
    drag.addPointer(down);
    tester.closeArena(1);

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
    expect(didStartPan, 0);

    tester.route(pointer.up());

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
    expect(didStartPan, 0);
  });

  testGesture('Should call start only once if there is a competing gesture recognizer', (GestureTester tester) {
    final PanGestureRecognizer drag = PanGestureRecognizer();

    // Device specific constants that represent those from the iPhone X
    const double pressureMin = 0;
    const double pressureMax = 6.66;

    int started = 0;
    int peaked = 0;
    int updated = 0;
    int ended = 0;

    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();

    force.onStart = (_) => started += 1;
    force.onPeak = (_) => peaked += 1;
    force.onUpdate = (_) => updated += 1;
    force.onEnd = (_) => ended += 1;

    int didStartPan = 0;
    drag.onStart = (_) => didStartPan += 1;

    const int pointerValue = 1;
    final TestPointer pointer = TestPointer();
    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressureMin: pressureMin, pressureMax: pressureMax);
    pointer.setDownInfo(down, const Offset(10.0, 10.0));
    force.addPointer(down);
    drag.addPointer(down);
    tester.closeArena(1);

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);
    expect(didStartPan, 0);

    // Pressure fed into the test environment simulates the values received directly from the device.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.0, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have not hit the start pressure, so no events should be true.
    expect(started, 1);
    expect(peaked, 0);
    expect(updated, 1);
    expect(ended, 0);
    expect(didStartPan, 0);

    tester.route(pointer.up());

    expect(started, 1);
    expect(peaked, 0);
    expect(updated, 1);
    expect(ended, 1);
    expect(didStartPan, 0);
  });

  testGesture('A force press can be recognized with a custom interpolation function', (GestureTester tester) {

    // Device specific constants that represent those from the iPhone X
    const double pressureMin = 0;
    const double pressureMax = 6.66;

    int started = 0;
    int peaked = 0;
    int updated = 0;
    int ended = 0;

    Offset? startGlobalPosition;

    void onStart(ForcePressDetails details) {
      startGlobalPosition = details.globalPosition;
      started += 1;
    }

    double interpolateWithEasing(double min, double max, double t) {
      final double lerp = (t - min) / (max - min);
      return Curves.easeIn.transform(lerp);
    }

    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer(interpolation: interpolateWithEasing);

    force.onStart = onStart;
    force.onPeak = (ForcePressDetails details) => peaked += 1;
    force.onUpdate = (ForcePressDetails details) => updated += 1;
    force.onEnd = (ForcePressDetails details) => ended += 1;

    const int pointerValue = 1;
    final TestPointer pointer = TestPointer();
    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
    pointer.setDownInfo(down, const Offset(10.0, 10.0));
    force.addPointer(down);
    tester.closeArena(pointerValue);

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    // Pressure fed into the test environment simulates the values received directly from the device.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have not hit the start pressure, so no events should be true.
    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.8, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have just hit the start pressure so just the start event should be triggered and one update call should have occurred.
    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
    expect(started, 0);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
    expect(started, 1);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 5.0, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressureMin: pressureMin, pressureMax: pressureMax));

    // We have exceeded the start pressure so update should be greater than 0.
    expect(started, 1);
    expect(updated, 3);
    expect(peaked, 0);
    expect(ended, 0);
    expect(startGlobalPosition, const Offset(10.0, 10.0));

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 6.0, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have exceeded the peak pressure so peak pressure should be true.
    expect(started, 1);
    expect(updated, 4);
    expect(peaked, 0);
    expect(ended, 0);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 3.3, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 4.0, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 6.5, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressureMin: pressureMin, pressureMax: pressureMax));

    // Update is still called.
    expect(started, 1);
    expect(updated, 8);
    expect(peaked, 1);
    expect(ended, 0);

    tester.route(pointer.up());

    // We have ended the gesture so ended should be true.
    expect(started, 1);
    expect(updated, 8);
    expect(peaked, 1);
    expect(ended, 1);
  });

  testGesture('A pressure outside of the device reported min and max pressure will not give an error', (GestureTester tester) {
    // Device specific constants that represent those from the iPhone X
    const double pressureMin = 0;
    const double pressureMax = 6.66;

    int started = 0;
    int peaked = 0;
    int updated = 0;
    int ended = 0;

    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();

    force.onStart = (_) => started += 1;
    force.onPeak = (_) => peaked += 1;
    force.onUpdate = (_) => updated += 1;
    force.onEnd = (_) => ended += 1;

    const int pointerValue = 1;
    final TestPointer pointer = TestPointer();
    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
    pointer.setDownInfo(down, const Offset(10.0, 10.0));
    force.addPointer(down);
    tester.closeArena(1);

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    // Pressure fed into the test environment simulates the values received directly from the device.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have not hit the start pressure, so no events should be true.
    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    // If the case where the pressure is greater than the max pressure were not handled correctly, this move event would throw an error.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 8.0, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(pointer.up());

    expect(started, 1);
    expect(peaked, 1);
    expect(updated, 1);
    expect(ended, 1);
  });

  testGesture('A pressure of NAN will not give an error', (GestureTester tester) {
    // Device specific constants that represent those from the iPhone X
    const double pressureMin = 0;
    const double pressureMax = 6.66;

    int started = 0;
    int peaked = 0;
    int updated = 0;
    int ended = 0;

    final ForcePressGestureRecognizer force = ForcePressGestureRecognizer();

    force.onStart = (_) => started += 1;
    force.onPeak = (_) => peaked += 1;
    force.onUpdate = (_) => updated += 1;
    force.onEnd = (_) => ended += 1;

    const int pointerValue = 1;
    final TestPointer pointer = TestPointer();
    const PointerDownEvent down = PointerDownEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0, pressureMin: pressureMin, pressureMax: pressureMax);
    pointer.setDownInfo(down, const Offset(10.0, 10.0));
    force.addPointer(down);
    tester.closeArena(1);

    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    // Pressure fed into the test environment simulates the values received directly from the device.
    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 2.5, pressureMin: pressureMin, pressureMax: pressureMax));

    // We have not hit the start pressure, so no events should be true.
    expect(started, 0);
    expect(peaked, 0);
    expect(updated, 0);
    expect(ended, 0);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 6.0, pressureMin: pressureMin, pressureMax: pressureMax));
    expect(peaked, 1);

    tester.route(const PointerMoveEvent(pointer: pointerValue, position: Offset(10.0, 10.0), pressure: 0.0 / 0.0, pressureMin: pressureMin, pressureMax: pressureMax));
    tester.route(pointer.up());

    expect(started, 1);
    expect(peaked, 1);
    expect(updated, 1);
    expect(ended, 1);
  });
}
