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

import 'gesture_tester.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  test('acceptGesture tolerates a null lastPendingEventTimestamp', () {
    // Regression test for https://github.com/flutter/flutter/issues/112403
    // and b/249091367
    final DragGestureRecognizer recognizer = VerticalDragGestureRecognizer();
    const PointerDownEvent event = PointerDownEvent(timeStamp: Duration(days: 10));

    expect(recognizer.debugLastPendingEventTimestamp, null);

    recognizer.addAllowedPointer(event);
    expect(recognizer.debugLastPendingEventTimestamp, event.timeStamp);

    // Normal case: acceptGesture called and we have a last timestamp set.
    recognizer.acceptGesture(event.pointer);
    expect(recognizer.debugLastPendingEventTimestamp, null);

    // Reject the gesture to reset state and allow accepting it again.
    recognizer.rejectGesture(event.pointer);
    expect(recognizer.debugLastPendingEventTimestamp, null);

    // Not entirely clear how this can happen, but the bugs mentioned above show
    // we can end up in this state empircally.
    recognizer.acceptGesture(event.pointer);
    expect(recognizer.debugLastPendingEventTimestamp, null);
  });

  testGesture('do not crash on up event for a pending pointer after winning arena for another pointer', (GestureTester tester) {
    // Regression test for https://github.com/flutter/flutter/issues/75061.

    final VerticalDragGestureRecognizer v = VerticalDragGestureRecognizer()
      ..onStart = (_) { };
    final HorizontalDragGestureRecognizer h = HorizontalDragGestureRecognizer()
      ..onStart = (_) { };

    const PointerDownEvent down90 = PointerDownEvent(
      pointer: 90,
      position: Offset(10.0, 10.0),
    );

    const PointerUpEvent up90 = PointerUpEvent(
      pointer: 90,
      position: Offset(10.0, 10.0),
    );

    const PointerDownEvent down91 = PointerDownEvent(
      pointer: 91,
      position: Offset(20.0, 20.0),
    );

    const PointerUpEvent up91 = PointerUpEvent(
      pointer: 91,
      position: Offset(20.0, 20.0),
    );

    v.addPointer(down90);
    GestureBinding.instance.gestureArena.close(90);
    h.addPointer(down91);
    v.addPointer(down91);
    GestureBinding.instance.gestureArena.close(91);
    tester.async.flushMicrotasks();

    GestureBinding.instance.handleEvent(up90, HitTestEntry(MockHitTestTarget()));
    GestureBinding.instance.handleEvent(up91, HitTestEntry(MockHitTestTarget()));
  });

  testGesture('DragGestureRecognizer should not dispatch drag callbacks when it wins the arena if onlyAcceptDragOnThreshold is true and the threshold has not been met', (GestureTester tester) {
    final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer();
    final List<String> dragCallbacks = <String>[];
    verticalDrag
      ..onlyAcceptDragOnThreshold = true
      ..onStart = (DragStartDetails details) {
        dragCallbacks.add('onStart');
      }
      ..onUpdate = (DragUpdateDetails details) {
        dragCallbacks.add('onUpdate');
      }
      ..onEnd = (DragEndDetails details) {
        dragCallbacks.add('onEnd');
      };

    const PointerDownEvent down1 = PointerDownEvent(
      pointer: 6,
      position: Offset(10.0, 10.0),
    );

    const PointerUpEvent up1 = PointerUpEvent(
      pointer: 6,
      position: Offset(10.0, 10.0),
    );

    verticalDrag.addPointer(down1);
    tester.closeArena(down1.pointer);
    tester.route(down1);
    tester.route(up1);
    expect(dragCallbacks.isEmpty, true);
    verticalDrag.dispose();
    dragCallbacks.clear();
  });

  testGesture('DragGestureRecognizer should dispatch drag callbacks when it wins the arena if onlyAcceptDragOnThreshold is false and the threshold has not been met', (GestureTester tester) {
    final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer();
    final List<String> dragCallbacks = <String>[];
    verticalDrag
      ..onlyAcceptDragOnThreshold = false
      ..onStart = (DragStartDetails details) {
        dragCallbacks.add('onStart');
      }
      ..onUpdate = (DragUpdateDetails details) {
        dragCallbacks.add('onUpdate');
      }
      ..onEnd = (DragEndDetails details) {
        dragCallbacks.add('onEnd');
      };

    const PointerDownEvent down1 = PointerDownEvent(
      pointer: 6,
      position: Offset(10.0, 10.0),
    );

    const PointerUpEvent up1 = PointerUpEvent(
      pointer: 6,
      position: Offset(10.0, 10.0),
    );

    verticalDrag.addPointer(down1);
    tester.closeArena(down1.pointer);
    tester.route(down1);
    tester.route(up1);
    expect(dragCallbacks.isEmpty, false);
    expect(dragCallbacks, <String>['onStart', 'onEnd']);
    verticalDrag.dispose();
    dragCallbacks.clear();
  });

  group('Recognizers on different button filters:', () {
    final List<String> recognized = <String>[];
    late HorizontalDragGestureRecognizer primaryRecognizer;
    late HorizontalDragGestureRecognizer secondaryRecognizer;
    setUp(() {
      primaryRecognizer = HorizontalDragGestureRecognizer(
          allowedButtonsFilter: (int buttons) => kPrimaryButton == buttons)
        ..onStart = (DragStartDetails details) {
          recognized.add('onStartPrimary');
        };
      secondaryRecognizer = HorizontalDragGestureRecognizer(
          allowedButtonsFilter: (int buttons) => kSecondaryButton == buttons)
        ..onStart = (DragStartDetails details) {
          recognized.add('onStartSecondary');
        };
    });

    tearDown(() {
      recognized.clear();
      primaryRecognizer.dispose();
      secondaryRecognizer.dispose();
    });

    testGesture('Primary button works', (GestureTester tester) {
      const PointerDownEvent down1 = PointerDownEvent(
        pointer: 6,
        position: Offset(10.0, 10.0),
      );

      primaryRecognizer.addPointer(down1);
      secondaryRecognizer.addPointer(down1);
      tester.closeArena(down1.pointer);
      tester.route(down1);
      expect(recognized, <String>['onStartPrimary']);
    });

    testGesture('Secondary button works', (GestureTester tester) {
      const PointerDownEvent down1 = PointerDownEvent(
        pointer: 6,
        position: Offset(10.0, 10.0),
        buttons: kSecondaryMouseButton,
      );

      primaryRecognizer.addPointer(down1);
      secondaryRecognizer.addPointer(down1);
      tester.closeArena(down1.pointer);
      tester.route(down1);
      expect(recognized, <String>['onStartSecondary']);
    });
  });
}

class MockHitTestTarget implements HitTestTarget {
  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) { }
}