// 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:ui' show VoidCallback;

import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';

import 'gesture_tester.dart';

// Down/move/up pair 1: normal tap sequence
const PointerDownEvent down = PointerDownEvent(
  pointer: 5,
  position: Offset(10, 10),
);

const PointerMoveEvent move = PointerMoveEvent(
  pointer: 5,
  position: Offset(15, 15),
);

const PointerUpEvent up = PointerUpEvent(
  pointer: 5,
  position: Offset(15, 15),
);

// Down/move/up pair 2: tap sequence with a large move in the middle
const PointerDownEvent down2 = PointerDownEvent(
  pointer: 6,
  position: Offset(10, 10),
);

const PointerMoveEvent move2 = PointerMoveEvent(
  pointer: 6,
  position: Offset(100, 200),
);

const PointerUpEvent up2 = PointerUpEvent(
  pointer: 6,
  position: Offset(100, 200),
);

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  test('GestureRecognizer smoketest', () {
    final TestGestureRecognizer recognizer = TestGestureRecognizer(debugOwner: 0);
    expect(recognizer, hasAGoodToStringDeep);
  });

  test('OffsetPair', () {
    const OffsetPair offset1 = OffsetPair(
      local: Offset(10, 20),
      global: Offset(30, 40),
    );

    expect(offset1.local, const Offset(10, 20));
    expect(offset1.global, const Offset(30, 40));

    const OffsetPair offset2 = OffsetPair(
      local: Offset(50, 60),
      global: Offset(70, 80),
    );

    final OffsetPair sum = offset2 + offset1;
    expect(sum.local, const Offset(60, 80));
    expect(sum.global, const Offset(100, 120));

    final OffsetPair difference = offset2 - offset1;
    expect(difference.local, const Offset(40, 40));
    expect(difference.global, const Offset(40, 40));
  });

  group('PrimaryPointerGestureRecognizer', () {
    testGesture('cleans up state after winning arena', (GestureTester tester) {
      final List<String> resolutions = <String>[];
      final IndefiniteGestureRecognizer indefinite = IndefiniteGestureRecognizer();
      final TestPrimaryPointerGestureRecognizer<PointerUpEvent> accepting = TestPrimaryPointerGestureRecognizer<PointerUpEvent>(
        GestureDisposition.accepted,
        onAcceptGesture: () => resolutions.add('accepted'),
        onRejectGesture: () => resolutions.add('rejected'),
      );
      expect(accepting.state, GestureRecognizerState.ready);
      expect(accepting.primaryPointer, isNull);
      expect(accepting.initialPosition, isNull);
      expect(resolutions, <String>[]);

      indefinite.addPointer(down);
      accepting.addPointer(down);
      expect(accepting.state, GestureRecognizerState.possible);
      expect(accepting.primaryPointer, 5);
      expect(accepting.initialPosition!.global, down.position);
      expect(accepting.initialPosition!.local, down.localPosition);
      expect(resolutions, <String>[]);

      tester.closeArena(5);
      tester.async.flushMicrotasks();
      tester.route(down);
      tester.route(up);
      expect(accepting.state, GestureRecognizerState.ready);
      expect(accepting.primaryPointer, 5);
      expect(accepting.initialPosition, isNull);
      expect(resolutions, <String>['accepted']);
    });

    testGesture('cleans up state after losing arena', (GestureTester tester) {
      final List<String> resolutions = <String>[];
      final IndefiniteGestureRecognizer indefinite = IndefiniteGestureRecognizer();
      final TestPrimaryPointerGestureRecognizer<PointerMoveEvent> rejecting = TestPrimaryPointerGestureRecognizer<PointerMoveEvent>(
        GestureDisposition.rejected,
        onAcceptGesture: () => resolutions.add('accepted'),
        onRejectGesture: () => resolutions.add('rejected'),
      );
      expect(rejecting.state, GestureRecognizerState.ready);
      expect(rejecting.primaryPointer, isNull);
      expect(rejecting.initialPosition, isNull);
      expect(resolutions, <String>[]);

      indefinite.addPointer(down);
      rejecting.addPointer(down);
      expect(rejecting.state, GestureRecognizerState.possible);
      expect(rejecting.primaryPointer, 5);
      expect(rejecting.initialPosition!.global, down.position);
      expect(rejecting.initialPosition!.local, down.localPosition);
      expect(resolutions, <String>[]);

      tester.closeArena(5);
      tester.async.flushMicrotasks();
      tester.route(down);
      tester.route(move);
      expect(rejecting.state, GestureRecognizerState.defunct);
      expect(rejecting.primaryPointer, 5);
      expect(rejecting.initialPosition!.global, down.position);
      expect(rejecting.initialPosition!.local, down.localPosition);
      expect(resolutions, <String>['rejected']);

      tester.route(up);
      expect(rejecting.state, GestureRecognizerState.ready);
      expect(rejecting.primaryPointer, 5);
      expect(rejecting.initialPosition, isNull);
      expect(resolutions, <String>['rejected']);
    });

    testGesture('works properly when recycled', (GestureTester tester) {
      final List<String> resolutions = <String>[];
      final IndefiniteGestureRecognizer indefinite = IndefiniteGestureRecognizer();
      final TestPrimaryPointerGestureRecognizer<PointerUpEvent> accepting = TestPrimaryPointerGestureRecognizer<PointerUpEvent>(
        GestureDisposition.accepted,
        preAcceptSlopTolerance: 15,
        postAcceptSlopTolerance: 1000,
        onAcceptGesture: () => resolutions.add('accepted'),
        onRejectGesture: () => resolutions.add('rejected'),
      );

      // Send one complete pointer sequence
      indefinite.addPointer(down);
      accepting.addPointer(down);
      tester.closeArena(5);
      tester.async.flushMicrotasks();
      tester.route(down);
      tester.route(up);
      expect(resolutions, <String>['accepted']);
      resolutions.clear();

      // Send a follow-on sequence that breaks preAcceptSlopTolerance
      indefinite.addPointer(down2);
      accepting.addPointer(down2);
      tester.closeArena(6);
      tester.async.flushMicrotasks();
      tester.route(down2);
      tester.route(move2);
      expect(resolutions, <String>['rejected']);
      tester.route(up2);
      expect(resolutions, <String>['rejected']);
    });
  });
}

class TestGestureRecognizer extends GestureRecognizer {
  TestGestureRecognizer({ super.debugOwner });

  @override
  String get debugDescription => 'debugDescription content';

  @override
  void addPointer(PointerDownEvent event) { }

  @override
  void acceptGesture(int pointer) { }

  @override
  void rejectGesture(int pointer) { }
}

/// Gesture recognizer that adds itself to the gesture arena but never
/// resolves itself.
class IndefiniteGestureRecognizer extends GestureRecognizer {
  @override
  void addAllowedPointer(PointerDownEvent event) {
    GestureBinding.instance.gestureArena.add(event.pointer, this);
  }

  @override
  void acceptGesture(int pointer) { }

  @override
  void rejectGesture(int pointer) { }

  @override
  String get debugDescription => 'Unresolving';
}

/// Gesture recognizer that resolves with [resolution] when it handles an event
/// on the primary pointer of type [T]
class TestPrimaryPointerGestureRecognizer<T extends PointerEvent> extends PrimaryPointerGestureRecognizer {
  TestPrimaryPointerGestureRecognizer(
    this.resolution, {
    this.onAcceptGesture,
    this.onRejectGesture,
    super.preAcceptSlopTolerance,
    super.postAcceptSlopTolerance,
  });

  final GestureDisposition resolution;
  final VoidCallback? onAcceptGesture;
  final VoidCallback? onRejectGesture;

  @override
  void acceptGesture(int pointer) {
    super.acceptGesture(pointer);
    if (onAcceptGesture != null) {
      onAcceptGesture!();
    }
  }

  @override
  void rejectGesture(int pointer) {
    super.rejectGesture(pointer);
    if (onRejectGesture != null) {
      onRejectGesture!();
    }
  }

  @override
  void handlePrimaryPointer(PointerEvent event) {
    if (event is T) {
      resolve(resolution);
    }
  }

  @override
  String get debugDescription => 'TestPrimaryPointer';
}
