// Copyright 2013 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 <Foundation/Foundation.h>
#import <OCMock/OCMock.h>
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#include <_types/_uint32_t.h>

#include "flutter/fml/platform/darwin/message_loop_darwin.h"
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
#import "flutter/shell/platform/darwin/ios/InternalFlutterSwift/InternalFlutterSwift.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"

FLUTTER_ASSERT_ARC;

namespace flutter {
class PointerDataPacket {};
}  // namespace flutter

using namespace flutter::testing;

namespace {

typedef void (^KeyCallbackSetter)(FlutterUIPressProxy* press, FlutterAsyncKeyCallback callback)
    API_AVAILABLE(ios(13.4));
typedef BOOL (^BoolGetter)();

}  // namespace

// These tests were designed to run on iOS 13.4 or later.
API_AVAILABLE(ios(13.4))
@interface FlutterKeyboardManagerTest : XCTestCase
@end

@implementation FlutterKeyboardManagerTest

- (id<FlutterKeyPrimaryResponder>)mockPrimaryResponder:(KeyCallbackSetter)callbackSetter {
  id<FlutterKeyPrimaryResponder> mock =
      OCMStrictProtocolMock(@protocol(FlutterKeyPrimaryResponder));
  OCMStub([mock handlePress:[OCMArg any] callback:[OCMArg any]])
      .andDo((^(NSInvocation* invocation) {
        __unsafe_unretained FlutterUIPressProxy* pressUnsafe;
        __unsafe_unretained FlutterAsyncKeyCallback callbackUnsafe;

        [invocation getArgument:&pressUnsafe atIndex:2];
        [invocation getArgument:&callbackUnsafe atIndex:3];

        // Retain the unretained parameters so they can
        // be run in the perform block when this invocation goes out of scope.
        FlutterUIPressProxy* press = pressUnsafe;
        FlutterAsyncKeyCallback callback = callbackUnsafe;
        CFRunLoopPerformBlock(CFRunLoopGetCurrent(),
                              fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, ^() {
                                callbackSetter(press, callback);
                              });
      }));
  return mock;
}

- (id<FlutterKeySecondaryResponder>)mockSecondaryResponder:(BoolGetter)resultGetter {
  id<FlutterKeySecondaryResponder> mock =
      OCMStrictProtocolMock(@protocol(FlutterKeySecondaryResponder));
  OCMStub([mock handlePress:[OCMArg any]]).andDo((^(NSInvocation* invocation) {
    BOOL result = resultGetter();
    [invocation setReturnValue:&result];
  }));
  return mock;
}

- (void)testNextResponderShouldThrowOnPressesEnded {
  // The nextResponder is a strict mock and hasn't stubbed pressesEnded.
  // An error will be thrown on pressesEnded.
  UIResponder* nextResponder = OCMStrictClassMock([UIResponder class]);
  OCMStub([nextResponder pressesBegan:OCMOCK_ANY withEvent:OCMOCK_ANY]);

  id mockEngine = OCMClassMock([FlutterEngine class]);
  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
                                                                                nibName:nil
                                                                                 bundle:nil];
  FlutterViewController* owner = OCMPartialMock(viewController);
  OCMStub([owner nextResponder]).andReturn(nextResponder);

  XCTAssertThrowsSpecificNamed([owner.nextResponder pressesEnded:[[NSSet alloc] init]
                                                       withEvent:[[UIPressesEvent alloc] init]],
                               NSException, NSInternalInconsistencyException);

  [mockEngine stopMocking];
}

- (void)testSinglePrimaryResponder {
  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
  __block BOOL primaryResponse = FALSE;
  __block int callbackCount = 0;
  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
                                                            FlutterAsyncKeyCallback callback) {
             callbackCount++;
             callback(primaryResponse);
           }]];
  constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
  // Case: The responder reports TRUE
  __block bool completeHandled = true;
  primaryResponse = TRUE;
  [manager handlePress:keyDownEvent(keyId)
            nextAction:^() {
              completeHandled = false;
            }];
  XCTAssertEqual(callbackCount, 1);
  XCTAssertTrue(completeHandled);
  completeHandled = true;
  callbackCount = 0;

  // Case: The responder reports FALSE
  primaryResponse = FALSE;
  [manager handlePress:keyUpEvent(keyId)
            nextAction:^() {
              completeHandled = false;
            }];
  XCTAssertEqual(callbackCount, 1);
  XCTAssertFalse(completeHandled);
}

- (void)testDoublePrimaryResponder {
  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];

  __block BOOL callback1Response = FALSE;
  __block int callback1Count = 0;
  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
                                                            FlutterAsyncKeyCallback callback) {
             callback1Count++;
             callback(callback1Response);
           }]];

  __block BOOL callback2Response = FALSE;
  __block int callback2Count = 0;
  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
                                                            FlutterAsyncKeyCallback callback) {
             callback2Count++;
             callback(callback2Response);
           }]];

  // Case: Both responders report TRUE.
  __block bool somethingWasHandled = true;
  constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
  callback1Response = TRUE;
  callback2Response = TRUE;
  [manager handlePress:keyUpEvent(keyId)
            nextAction:^() {
              somethingWasHandled = false;
            }];
  XCTAssertEqual(callback1Count, 1);
  XCTAssertEqual(callback2Count, 1);
  XCTAssertTrue(somethingWasHandled);

  somethingWasHandled = true;
  callback1Count = 0;
  callback2Count = 0;

  // Case: One responder reports TRUE.
  callback1Response = TRUE;
  callback2Response = FALSE;
  [manager handlePress:keyUpEvent(keyId)
            nextAction:^() {
              somethingWasHandled = false;
            }];
  XCTAssertEqual(callback1Count, 1);
  XCTAssertEqual(callback2Count, 1);
  XCTAssertTrue(somethingWasHandled);

  somethingWasHandled = true;
  callback1Count = 0;
  callback2Count = 0;

  // Case: Both responders report FALSE.
  callback1Response = FALSE;
  callback2Response = FALSE;
  [manager handlePress:keyDownEvent(keyId)
            nextAction:^() {
              somethingWasHandled = false;
            }];
  XCTAssertEqual(callback1Count, 1);
  XCTAssertEqual(callback2Count, 1);
  XCTAssertFalse(somethingWasHandled);
}

- (void)testSingleSecondaryResponder {
  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];

  __block BOOL primaryResponse = FALSE;
  __block int callbackCount = 0;
  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
                                                            FlutterAsyncKeyCallback callback) {
             callbackCount++;
             callback(primaryResponse);
           }]];

  __block BOOL secondaryResponse;
  [manager addSecondaryResponder:[self mockSecondaryResponder:^() {
             return secondaryResponse;
           }]];

  // Case: Primary responder responds TRUE. The event shouldn't be handled by
  // the secondary responder.
  constexpr UIKeyboardHIDUsage keyId = (UIKeyboardHIDUsage)0x50;
  secondaryResponse = FALSE;
  primaryResponse = TRUE;
  __block bool completeHandled = true;
  [manager handlePress:keyUpEvent(keyId)
            nextAction:^() {
              completeHandled = false;
            }];
  XCTAssertEqual(callbackCount, 1);
  XCTAssertTrue(completeHandled);
  completeHandled = true;
  callbackCount = 0;

  // Case: Primary responder responds FALSE. The secondary responder returns
  // TRUE.
  secondaryResponse = TRUE;
  primaryResponse = FALSE;
  [manager handlePress:keyUpEvent(keyId)
            nextAction:^() {
              completeHandled = false;
            }];
  XCTAssertEqual(callbackCount, 1);
  XCTAssertTrue(completeHandled);
  completeHandled = true;
  callbackCount = 0;

  // Case: Primary responder responds FALSE. The secondary responder returns FALSE.
  secondaryResponse = FALSE;
  primaryResponse = FALSE;
  [manager handlePress:keyDownEvent(keyId)
            nextAction:^() {
              completeHandled = false;
            }];
  XCTAssertEqual(callbackCount, 1);
  XCTAssertFalse(completeHandled);
}

- (void)testEventsProcessedSequentially {
  constexpr UIKeyboardHIDUsage keyId1 = (UIKeyboardHIDUsage)0x50;
  constexpr UIKeyboardHIDUsage keyId2 = (UIKeyboardHIDUsage)0x51;
  FlutterUIPressProxy* event1 = keyDownEvent(keyId1);
  FlutterUIPressProxy* event2 = keyDownEvent(keyId2);
  __block FlutterAsyncKeyCallback key1Callback;
  __block FlutterAsyncKeyCallback key2Callback;
  __block bool key1Handled = true;
  __block bool key2Handled = true;

  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
                                                            FlutterAsyncKeyCallback callback) {
             if (press == event1) {
               key1Callback = callback;
             } else if (press == event2) {
               key2Callback = callback;
             }
           }]];

  // Add both presses into the main CFRunLoop queue
  CFRunLoopTimerRef timer0 = CFRunLoopTimerCreateWithHandler(
      kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
        [manager handlePress:event1
                  nextAction:^() {
                    key1Handled = false;
                  }];
      });
  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer0, kCFRunLoopCommonModes);
  CFRunLoopTimerRef timer1 = CFRunLoopTimerCreateWithHandler(
      kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 1, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
        // key1 should be completely finished by now
        XCTAssertFalse(key1Handled);
        [manager handlePress:event2
                  nextAction:^() {
                    key2Handled = false;
                  }];
        // End the nested CFRunLoop
        CFRunLoopStop(CFRunLoopGetCurrent());
      });
  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer1, kCFRunLoopCommonModes);

  // Add the callbacks to the CFRunLoop with mode kMessageLoopCFRunLoopMode
  // This allows them to interrupt the loop started within handlePress
  CFRunLoopTimerRef timer2 = CFRunLoopTimerCreateWithHandler(
      kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 2, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
        // No processing should be done on key2 yet
        XCTAssertTrue(key1Callback != nil);
        XCTAssertTrue(key2Callback == nil);
        key1Callback(false);
      });
  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer2,
                    fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
  CFRunLoopTimerRef timer3 = CFRunLoopTimerCreateWithHandler(
      kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 3, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
        // Both keys should be processed by now
        XCTAssertTrue(key1Callback != nil);
        XCTAssertTrue(key2Callback != nil);
        key2Callback(false);
      });
  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer3,
                    fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);

  // Start a nested CFRunLoop so we can wait for both presses to complete before exiting the test
  CFRunLoopRun();
  XCTAssertFalse(key2Handled);
  XCTAssertFalse(key1Handled);
}

@end
