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

import 'gesture_tester.dart';

void main() {
  setUp(ensureGestureBinding);

  testGesture('Should recognize scale gestures', (GestureTester tester) {
    final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
    final TapGestureRecognizer tap = TapGestureRecognizer();

    bool didStartScale = false;
    Offset? updatedFocalPoint;
    scale.onStart = (ScaleStartDetails details) {
      didStartScale = true;
      updatedFocalPoint = details.focalPoint;
    };

    double? updatedScale;
    double? updatedHorizontalScale;
    double? updatedVerticalScale;
    scale.onUpdate = (ScaleUpdateDetails details) {
      updatedScale = details.scale;
      updatedHorizontalScale = details.horizontalScale;
      updatedVerticalScale = details.verticalScale;
      updatedFocalPoint = details.focalPoint;
    };

    bool didEndScale = false;
    scale.onEnd = (ScaleEndDetails details) {
      didEndScale = true;
    };

    bool didTap = false;
    tap.onTap = () {
      didTap = true;
    };

    final TestPointer pointer1 = TestPointer(1);

    final PointerDownEvent down = pointer1.down(const Offset(0.0, 0.0));
    scale.addPointer(down);
    tap.addPointer(down);

    tester.closeArena(1);
    expect(didStartScale, isFalse);
    expect(updatedScale, isNull);
    expect(updatedFocalPoint, isNull);
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    // One-finger panning
    tester.route(down);
    expect(didStartScale, isFalse);
    expect(updatedScale, isNull);
    expect(updatedFocalPoint, isNull);
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    tester.route(pointer1.move(const Offset(20.0, 30.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedFocalPoint, const Offset(20.0, 30.0));
    updatedFocalPoint = null;
    expect(updatedScale, 1.0);
    updatedScale = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    // Two-finger scaling
    final TestPointer pointer2 = TestPointer(2);
    final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0));
    scale.addPointer(down2);
    tap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);

    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(updatedScale, isNull);
    expect(updatedFocalPoint, isNull);
    expect(didStartScale, isFalse);

    // Zoom in
    tester.route(pointer2.move(const Offset(0.0, 10.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedFocalPoint, const Offset(10.0, 20.0));
    updatedFocalPoint = null;
    expect(updatedScale, 2.0);
    expect(updatedHorizontalScale, 2.0);
    expect(updatedVerticalScale, 2.0);
    updatedScale = null;
    updatedHorizontalScale = null;
    updatedVerticalScale = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    // Zoom out
    tester.route(pointer2.move(const Offset(15.0, 25.0)));
    expect(updatedFocalPoint, const Offset(17.5, 27.5));
    expect(updatedScale, 0.5);
    expect(updatedHorizontalScale, 0.5);
    expect(updatedVerticalScale, 0.5);
    expect(didTap, isFalse);

    // Horizontal scaling
    tester.route(pointer2.move(const Offset(0.0, 20.0)));
    expect(updatedHorizontalScale, 2.0);
    expect(updatedVerticalScale, 1.0);

    // Vertical scaling
    tester.route(pointer2.move(const Offset(10.0, 10.0)));
    expect(updatedHorizontalScale, 1.0);
    expect(updatedVerticalScale, 2.0);
    tester.route(pointer2.move(const Offset(15.0, 25.0)));
    updatedFocalPoint = null;
    updatedScale = null;

    // Three-finger scaling
    final TestPointer pointer3 = TestPointer(3);
    final PointerDownEvent down3 = pointer3.down(const Offset(25.0, 35.0));
    scale.addPointer(down3);
    tap.addPointer(down3);
    tester.closeArena(3);
    tester.route(down3);

    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(updatedScale, isNull);
    expect(updatedFocalPoint, isNull);
    expect(didStartScale, isFalse);

    // Zoom in
    tester.route(pointer3.move(const Offset(55.0, 65.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedFocalPoint, const Offset(30.0, 40.0));
    updatedFocalPoint = null;
    expect(updatedScale, 5.0);
    updatedScale = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    // Return to original positions but with different fingers
    tester.route(pointer1.move(const Offset(25.0, 35.0)));
    tester.route(pointer2.move(const Offset(20.0, 30.0)));
    tester.route(pointer3.move(const Offset(15.0, 25.0)));
    expect(didStartScale, isFalse);
    expect(updatedFocalPoint, const Offset(20.0, 30.0));
    updatedFocalPoint = null;
    expect(updatedScale, 1.0);
    updatedScale = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    tester.route(pointer1.up());
    expect(didStartScale, isFalse);
    expect(updatedFocalPoint, isNull);
    expect(updatedScale, isNull);
    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(didTap, isFalse);

    // Continue scaling with two fingers
    tester.route(pointer3.move(const Offset(10.0, 20.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedFocalPoint, const Offset(15.0, 25.0));
    updatedFocalPoint = null;
    expect(updatedScale, 2.0);
    updatedScale = null;

    // Continue rotating with two fingers
    tester.route(pointer3.move(const Offset(30.0, 40.0)));
    expect(updatedFocalPoint, const Offset(25.0, 35.0));
    updatedFocalPoint = null;
    expect(updatedScale, 2.0);
    updatedScale = null;
    tester.route(pointer3.move(const Offset(10.0, 20.0)));
    expect(updatedFocalPoint, const Offset(15.0, 25.0));
    updatedFocalPoint = null;
    expect(updatedScale, 2.0);
    updatedScale = null;

    tester.route(pointer2.up());
    expect(didStartScale, isFalse);
    expect(updatedFocalPoint, isNull);
    expect(updatedScale, isNull);
    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(didTap, isFalse);

    // Continue panning with one finger
    tester.route(pointer3.move(const Offset(0.0, 0.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedFocalPoint, const Offset(0.0, 0.0));
    updatedFocalPoint = null;
    expect(updatedScale, 1.0);
    updatedScale = null;

    // We are done
    tester.route(pointer3.up());
    expect(didStartScale, isFalse);
    expect(updatedFocalPoint, isNull);
    expect(updatedScale, isNull);
    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(didTap, isFalse);

    scale.dispose();
    tap.dispose();
  });

  testGesture('Rejects scale gestures from unallowed device kinds', (GestureTester tester) {
    final ScaleGestureRecognizer scale = ScaleGestureRecognizer(kind: PointerDeviceKind.touch);

    bool didStartScale = false;
    scale.onStart = (ScaleStartDetails details) {
      didStartScale = true;
    };

    double? updatedScale;
    scale.onUpdate = (ScaleUpdateDetails details) {
      updatedScale = details.scale;
    };

    final TestPointer mousePointer = TestPointer(1, PointerDeviceKind.mouse);

    final PointerDownEvent down = mousePointer.down(const Offset(0.0, 0.0));
    scale.addPointer(down);
    tester.closeArena(1);

    // One-finger panning
    tester.route(down);
    expect(didStartScale, isFalse);
    expect(updatedScale, isNull);

    // Using a mouse, the scale gesture shouldn't even start.
    tester.route(mousePointer.move(const Offset(20.0, 30.0)));
    expect(didStartScale, isFalse);
    expect(updatedScale, isNull);

    scale.dispose();
  });

  testGesture('Scale gestures starting from allowed device kinds cannot be ended from unallowed devices', (GestureTester tester) {
    final ScaleGestureRecognizer scale = ScaleGestureRecognizer(kind: PointerDeviceKind.touch);

    bool didStartScale = false;
    Offset? updatedFocalPoint;
    scale.onStart = (ScaleStartDetails details) {
      didStartScale = true;
      updatedFocalPoint = details.focalPoint;
    };

    double? updatedScale;
    scale.onUpdate = (ScaleUpdateDetails details) {
      updatedScale = details.scale;
      updatedFocalPoint = details.focalPoint;
    };

    bool didEndScale = false;
    scale.onEnd = (ScaleEndDetails details) {
      didEndScale = true;
    };

    final TestPointer touchPointer = TestPointer(1, PointerDeviceKind.touch);

    final PointerDownEvent down = touchPointer.down(const Offset(0.0, 0.0));
    scale.addPointer(down);
    tester.closeArena(1);

    // One-finger panning
    tester.route(down);
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedScale, isNull);
    expect(updatedFocalPoint, const Offset(0.0, 0.0));
    expect(didEndScale, isFalse);

    // The gesture can start using one touch finger.
    tester.route(touchPointer.move(const Offset(20.0, 30.0)));
    expect(updatedFocalPoint, const Offset(20.0, 30.0));
    updatedFocalPoint = null;
    expect(updatedScale, 1.0);
    updatedScale = null;
    expect(didEndScale, isFalse);

    // Two-finger scaling
    final TestPointer mousePointer = TestPointer(2, PointerDeviceKind.mouse);
    final PointerDownEvent down2 = mousePointer.down(const Offset(10.0, 20.0));
    scale.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);

    // Mouse-generated events are ignored.
    expect(didEndScale, isFalse);
    expect(updatedScale, isNull);
    expect(didStartScale, isFalse);

    // Zoom in using a mouse doesn't work either.
    tester.route(mousePointer.move(const Offset(0.0, 10.0)));
    expect(updatedScale, isNull);
    expect(didEndScale, isFalse);

    scale.dispose();
  });

  testGesture('Scale gesture competes with drag', (GestureTester tester) {
    final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
    final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();

    final List<String> log = <String>[];

    scale.onStart = (ScaleStartDetails details) { log.add('scale-start'); };
    scale.onUpdate = (ScaleUpdateDetails details) { log.add('scale-update'); };
    scale.onEnd = (ScaleEndDetails details) { log.add('scale-end'); };

    drag.onStart = (DragStartDetails details) { log.add('drag-start'); };
    drag.onEnd = (DragEndDetails details) { log.add('drag-end'); };

    final TestPointer pointer1 = TestPointer(1);

    final PointerDownEvent down = pointer1.down(const Offset(10.0, 10.0));
    scale.addPointer(down);
    drag.addPointer(down);

    tester.closeArena(1);
    expect(log, isEmpty);

    // Vertical moves are scales.
    tester.route(down);
    expect(log, isEmpty);

    // scale will win if focal point delta exceeds 18.0*2

    tester.route(pointer1.move(const Offset(10.0, 50.0))); // delta of 40.0 exceeds 18.0*2
    expect(log, equals(<String>['scale-start', 'scale-update']));
    log.clear();

    final TestPointer pointer2 = TestPointer(2);
    final PointerDownEvent down2 = pointer2.down(const Offset(10.0, 20.0));
    scale.addPointer(down2);
    drag.addPointer(down2);

    tester.closeArena(2);
    expect(log, isEmpty);

    // Second pointer joins scale even though it moves horizontally.
    tester.route(down2);
    expect(log, <String>['scale-end']);
    log.clear();

    tester.route(pointer2.move(const Offset(30.0, 20.0)));
    expect(log, equals(<String>['scale-start', 'scale-update']));
    log.clear();

    tester.route(pointer1.up());
    expect(log, equals(<String>['scale-end']));
    log.clear();

    tester.route(pointer2.up());
    expect(log, isEmpty);
    log.clear();

    // Horizontal moves are either drags or scales, depending on which wins first.
    // TODO(ianh): https://github.com/flutter/flutter/issues/11384
    // In this case, we move fast, so that the scale wins. If we moved slowly,
    // the horizontal drag would win, since it was added first.
    final TestPointer pointer3 = TestPointer(3);
    final PointerDownEvent down3 = pointer3.down(const Offset(30.0, 30.0));
    scale.addPointer(down3);
    drag.addPointer(down3);
    tester.closeArena(3);
    tester.route(down3);

    expect(log, isEmpty);

    tester.route(pointer3.move(const Offset(100.0, 30.0)));
    expect(log, equals(<String>['scale-start', 'scale-update']));
    log.clear();

    tester.route(pointer3.up());
    expect(log, equals(<String>['scale-end']));
    log.clear();

    scale.dispose();
    drag.dispose();
  });

  testGesture('Should recognize rotation gestures', (GestureTester tester) {
    final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
    final TapGestureRecognizer tap = TapGestureRecognizer();

    bool didStartScale = false;
    Offset? updatedFocalPoint;
    scale.onStart = (ScaleStartDetails details) {
      didStartScale = true;
      updatedFocalPoint = details.focalPoint;
    };

    double? updatedRotation;
    scale.onUpdate = (ScaleUpdateDetails details) {
      updatedRotation = details.rotation;
      updatedFocalPoint = details.focalPoint;
    };

    bool didEndScale = false;
    scale.onEnd = (ScaleEndDetails details) {
      didEndScale = true;
    };

    bool didTap = false;
    tap.onTap = () {
      didTap = true;
    };

    final TestPointer pointer1 = TestPointer(1);

    final PointerDownEvent down = pointer1.down(const Offset(0.0, 0.0));
    scale.addPointer(down);
    tap.addPointer(down);

    tester.closeArena(1);
    expect(didStartScale, isFalse);
    expect(updatedRotation, isNull);
    expect(updatedFocalPoint, isNull);
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    tester.route(down);
    tester.route(pointer1.move(const Offset(20.0, 30.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;

    expect(updatedFocalPoint, const Offset(20.0, 30.0));
    updatedFocalPoint = null;
    expect(updatedRotation, 0.0);
    updatedRotation = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    // Two-finger scaling
    final TestPointer pointer2 = TestPointer(2);
    final PointerDownEvent down2 = pointer2.down(const Offset(30.0, 40.0));
    scale.addPointer(down2);
    tap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);

    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(updatedFocalPoint, isNull);
    expect(updatedRotation, isNull);
    expect(didStartScale, isFalse);


    // Zoom in
    tester.route(pointer2.move(const Offset(40.0, 50.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedFocalPoint, const Offset(30.0, 40.0));
    updatedFocalPoint = null;
    expect(updatedRotation, 0.0);
    updatedRotation = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    // Rotation
    tester.route(pointer2.move(const Offset(0.0, 10.0)));
    expect(updatedFocalPoint, const Offset(10.0, 20.0));
    updatedFocalPoint = null;
    expect(updatedRotation, math.pi);
    updatedRotation = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    // Three-finger scaling
    final TestPointer pointer3 = TestPointer(3);
    final PointerDownEvent down3 = pointer3.down(const Offset(25.0, 35.0));
    scale.addPointer(down3);
    tap.addPointer(down3);
    tester.closeArena(3);
    tester.route(down3);

    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(updatedFocalPoint, isNull);
    expect(updatedRotation, isNull);
    expect(didStartScale, isFalse);

    // Zoom in
    tester.route(pointer3.move(const Offset(55.0, 65.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedFocalPoint, const Offset(25.0, 35.0));
    updatedFocalPoint = null;
    expect(updatedRotation, 0.0);
    updatedRotation = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    // Return to original positions but with different fingers
    tester.route(pointer1.move(const Offset(25.0, 35.0)));
    tester.route(pointer2.move(const Offset(20.0, 30.0)));
    tester.route(pointer3.move(const Offset(15.0, 25.0)));
    expect(didStartScale, isFalse);
    expect(updatedFocalPoint, const Offset(20.0, 30.0));
    updatedFocalPoint = null;
    expect(updatedRotation, 0.0);
    updatedRotation = null;
    expect(didEndScale, isFalse);
    expect(didTap, isFalse);

    tester.route(pointer1.up());
    expect(didStartScale, isFalse);
    expect(updatedFocalPoint, isNull);
    expect(updatedRotation, isNull);
    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(didTap, isFalse);


    // Continue scaling with two fingers
    tester.route(pointer3.move(const Offset(10.0, 20.0)));
    expect(didStartScale, isTrue);
    didStartScale = false;
    expect(updatedFocalPoint, const Offset(15.0, 25.0));
    updatedFocalPoint = null;
    expect(updatedRotation, 0.0);
    updatedRotation = null;

    // Continue rotating with two fingers
    tester.route(pointer3.move(const Offset(30.0, 40.0)));
    expect(updatedFocalPoint, const Offset(25.0, 35.0));
    updatedFocalPoint = null;
    expect(updatedRotation, - math.pi);
    updatedRotation = null;
    tester.route(pointer3.move(const Offset(10.0, 20.0)));
    expect(updatedFocalPoint, const Offset(15.0, 25.0));
    updatedFocalPoint = null;
    expect(updatedRotation, 0.0);
    updatedRotation = null;

    tester.route(pointer2.up());
    expect(didStartScale, isFalse);
    expect(updatedFocalPoint, isNull);
    expect(updatedRotation, isNull);
    expect(didEndScale, isTrue);
    didEndScale = false;
    expect(didTap, isFalse);

    // We are done
    tester.route(pointer3.up());
    expect(didStartScale, isFalse);
    expect(updatedFocalPoint, isNull);
    expect(updatedRotation, isNull);
    expect(didEndScale, isFalse);
    didEndScale = false;
    expect(didTap, isFalse);

    scale.dispose();
    tap.dispose();
  });
}
