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

import 'gesture_tester.dart';

// Anything longer than [kDoubleTapTimeout] will reset the serial tap count.
final Duration kSerialTapDelay = kDoubleTapTimeout ~/ 2;

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  late List<String> events;
  late SerialTapGestureRecognizer serial;

  setUp(() {
    events = <String>[];
    serial = SerialTapGestureRecognizer()
      ..onSerialTapDown = (SerialTapDownDetails details) {
        events.add('down#${details.count}');
      }
      ..onSerialTapCancel = (SerialTapCancelDetails details) {
        events.add('cancel#${details.count}');
      }
      ..onSerialTapUp = (SerialTapUpDetails details) {
        events.add('up#${details.count}');
      };
    addTearDown(serial.dispose);
  });

  // Down/up pair 1: normal tap sequence
  const PointerDownEvent down1 = PointerDownEvent(
    pointer: 1,
    position: Offset(10.0, 10.0),
  );

  const PointerCancelEvent cancel1 = PointerCancelEvent(
    pointer: 1,
  );

  const PointerUpEvent up1 = PointerUpEvent(
    pointer: 1,
    position: Offset(11.0, 9.0),
  );

  // Down/up pair 2: normal tap sequence close to pair 1
  const PointerDownEvent down2 = PointerDownEvent(
    pointer: 2,
    position: Offset(12.0, 12.0),
  );

  const PointerUpEvent up2 = PointerUpEvent(
    pointer: 2,
    position: Offset(13.0, 11.0),
  );

  // Down/up pair 3: normal tap sequence close to pair 1
  const PointerDownEvent down3 = PointerDownEvent(
    pointer: 3,
    position: Offset(12.0, 12.0),
  );

  const PointerUpEvent up3 = PointerUpEvent(
    pointer: 3,
    position: Offset(13.0, 11.0),
  );

  // Down/up pair 4: normal tap sequence far away from pair 1
  const PointerDownEvent down4 = PointerDownEvent(
    pointer: 4,
    position: Offset(130.0, 130.0),
  );

  const PointerUpEvent up4 = PointerUpEvent(
    pointer: 4,
    position: Offset(131.0, 129.0),
  );

  // Down/move/up sequence 5: intervening motion
  const PointerDownEvent down5 = PointerDownEvent(
    pointer: 5,
    position: Offset(10.0, 10.0),
  );

  const PointerMoveEvent move5 = PointerMoveEvent(
    pointer: 5,
    position: Offset(25.0, 25.0),
  );

  const PointerUpEvent up5 = PointerUpEvent(
    pointer: 5,
    position: Offset(25.0, 25.0),
  );

  // Down/up pair 7: normal tap sequence close to pair 1 but on secondary button
  const PointerDownEvent down6 = PointerDownEvent(
    pointer: 6,
    position: Offset(10.0, 10.0),
    buttons: kSecondaryMouseButton,
  );

  const PointerUpEvent up6 = PointerUpEvent(
    pointer: 6,
    position: Offset(11.0, 9.0),
  );

  testGesture('Recognizes serial taps', (GestureTester tester) {
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);

    events.clear();
    tester.async.elapse(kSerialTapDelay);
    serial.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
    GestureBinding.instance.gestureArena.sweep(2);
    expect(events, <String>['down#2', 'up#2']);

    events.clear();
    tester.async.elapse(kSerialTapDelay);
    serial.addPointer(down3);
    tester.closeArena(3);
    tester.route(down3);
    tester.route(up3);
    GestureBinding.instance.gestureArena.sweep(3);
    expect(events, <String>['down#3', 'up#3']);
  });

  // Because tap gesture will hold off on declaring victory.
  testGesture('Wins over tap gesture below it in the tree', (GestureTester tester) {
    bool recognizedSingleTap = false;
    bool canceledSingleTap = false;
    final TapGestureRecognizer singleTap = TapGestureRecognizer()
      ..onTap = () {
        recognizedSingleTap = true;
      }
      ..onTapCancel = () {
        canceledSingleTap = true;
      };
    addTearDown(singleTap.dispose);

    singleTap.addPointer(down1);
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.async.elapse(kPressTimeout); // To register the possible single tap.
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);
    expect(recognizedSingleTap, isFalse);
    expect(canceledSingleTap, isTrue);
  });

  testGesture('Wins over tap gesture above it in the tree', (GestureTester tester) {
    bool recognizedSingleTap = false;
    bool canceledSingleTap = false;
    final TapGestureRecognizer singleTap = TapGestureRecognizer()
      ..onTap = () {
        recognizedSingleTap = true;
      }
      ..onTapCancel = () {
        canceledSingleTap = true;
      };
    addTearDown(singleTap.dispose);

    serial.addPointer(down1);
    singleTap.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.async.elapse(kPressTimeout); // To register the possible single tap.
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);
    expect(recognizedSingleTap, isFalse);
    expect(canceledSingleTap, isTrue);
  });

  testGesture('Loses to release gesture below it in the tree', (GestureTester tester) {
    bool recognizedRelease = false;
    final ReleaseGestureRecognizer release = ReleaseGestureRecognizer()
      ..onRelease = () {
        recognizedRelease = true;
      };

    release.addPointer(down1);
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'cancel#1']);
    expect(recognizedRelease, isTrue);
  });

  testGesture('Wins over release gesture above it in the tree', (GestureTester tester) {
    bool recognizedRelease = false;
    final ReleaseGestureRecognizer release = ReleaseGestureRecognizer()
      ..onRelease = () {
        recognizedRelease = true;
      };

    serial.addPointer(down1);
    release.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);
    expect(recognizedRelease, isFalse);
  });

  testGesture('Fires cancel if competing recognizer declares victory', (GestureTester tester) {
    final WinningGestureRecognizer winner = WinningGestureRecognizer();
    winner.addPointer(down1);
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'cancel#1']);
  });

  testGesture('Wins over double-tap recognizer below it in the tree', (GestureTester tester) {
    bool recognizedDoubleTap = false;
    final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer()
      ..onDoubleTap = () {
        recognizedDoubleTap = true;
      };
    addTearDown(doubleTap.dispose);

    doubleTap.addPointer(down1);
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);
    expect(recognizedDoubleTap, isFalse);

    events.clear();
    tester.async.elapse(kSerialTapDelay);
    doubleTap.addPointer(down2);
    serial.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
    GestureBinding.instance.gestureArena.sweep(2);
    expect(events, <String>['down#2', 'up#2']);
    expect(recognizedDoubleTap, isFalse);

    events.clear();
    tester.async.elapse(kSerialTapDelay);
    serial.addPointer(down3);
    tester.closeArena(3);
    tester.route(down3);
    tester.route(up3);
    GestureBinding.instance.gestureArena.sweep(3);
    expect(events, <String>['down#3', 'up#3']);
  });

  testGesture('Wins over double-tap recognizer above it in the tree', (GestureTester tester) {
    bool recognizedDoubleTap = false;
    final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer()
      ..onDoubleTap = () {
        recognizedDoubleTap = true;
      };
    addTearDown(doubleTap.dispose);

    serial.addPointer(down1);
    doubleTap.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);
    expect(recognizedDoubleTap, isFalse);

    events.clear();
    tester.async.elapse(kSerialTapDelay);
    serial.addPointer(down2);
    doubleTap.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
    GestureBinding.instance.gestureArena.sweep(2);
    expect(events, <String>['down#2', 'up#2']);
    expect(recognizedDoubleTap, isFalse);

    events.clear();
    tester.async.elapse(kSerialTapDelay);
    serial.addPointer(down3);
    doubleTap.addPointer(down3);
    tester.closeArena(3);
    tester.route(down3);
    tester.route(up3);
    GestureBinding.instance.gestureArena.sweep(3);
    expect(events, <String>['down#3', 'up#3']);
  });

  testGesture('Fires cancel and resets for PointerCancelEvent', (GestureTester tester) {
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(cancel1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'cancel#1']);

    events.clear();
    tester.async.elapse(const Duration(milliseconds: 100));
    serial.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
    GestureBinding.instance.gestureArena.sweep(2);
    expect(events, <String>['down#1', 'up#1']);
  });

  testGesture('Fires cancel and resets when pointer dragged past slop tolerance', (GestureTester tester) {
    serial.addPointer(down5);
    tester.closeArena(5);
    tester.route(down5);
    tester.route(move5);
    tester.route(up5);
    GestureBinding.instance.gestureArena.sweep(5);
    expect(events, <String>['down#1', 'cancel#1']);

    events.clear();
    tester.async.elapse(const Duration(milliseconds: 1000));
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);
  });

  testGesture('Resets if times out in between taps', (GestureTester tester) {
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);

    events.clear();
    tester.async.elapse(const Duration(milliseconds: 1000));
    serial.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);
    tester.route(up2);
    GestureBinding.instance.gestureArena.sweep(2);
    expect(events, <String>['down#1', 'up#1']);
  });

  testGesture('Resets if taps are far apart', (GestureTester tester) {
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);

    events.clear();
    tester.async.elapse(const Duration(milliseconds: 100));
    serial.addPointer(down4);
    tester.closeArena(4);
    tester.route(down4);
    tester.route(up4);
    GestureBinding.instance.gestureArena.sweep(4);
    expect(events, <String>['down#1', 'up#1']);
  });

  testGesture('Serial taps with different buttons will start a new tap sequence', (GestureTester tester) {
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>['down#1', 'up#1']);

    events.clear();
    tester.async.elapse(const Duration(milliseconds: 1000));
    serial.addPointer(down6);
    tester.closeArena(6);
    tester.route(down6);
    tester.route(up6);
    GestureBinding.instance.gestureArena.sweep(6);
    expect(events, <String>['down#1', 'up#1']);
  });

  testGesture('Interleaving taps cancel first sequence and start second sequence', (GestureTester tester) {
    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);

    serial.addPointer(down2);
    tester.closeArena(2);
    tester.route(down2);

    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    tester.route(up2);
    GestureBinding.instance.gestureArena.sweep(2);
    expect(events, <String>['down#1', 'cancel#1', 'down#1', 'up#1']);
  });

  testGesture('Is no-op if no callbacks are specified', (GestureTester tester) {
    serial = SerialTapGestureRecognizer();
    addTearDown(serial.dispose);

    serial.addPointer(down1);
    tester.closeArena(1);
    tester.route(down1);
    expect(serial.isTrackingPointer, isFalse);
    tester.route(up1);
    GestureBinding.instance.gestureArena.sweep(1);
    expect(events, <String>[]);
  });

  testGesture('Works for non-primary button', (GestureTester tester) {
    serial.addPointer(down6);
    tester.closeArena(6);
    tester.route(down6);
    tester.route(up6);
    GestureBinding.instance.gestureArena.sweep(6);
    expect(events, <String>['down#1', 'up#1']);
  });
}

class WinningGestureRecognizer extends PrimaryPointerGestureRecognizer {
  @override
  String get debugDescription => 'winner';

  @override
  void handlePrimaryPointer(PointerEvent event) {
    resolve(GestureDisposition.accepted);
  }
}

class ReleaseGestureRecognizer extends PrimaryPointerGestureRecognizer {
  VoidCallback? onRelease;

  @override
  String get debugDescription => 'release';

  @override
  void handlePrimaryPointer(PointerEvent event) {
    if (event is PointerUpEvent) {
      resolve(GestureDisposition.accepted);
      if (onRelease != null) {
        onRelease!();
      }
    }
  }
}