/*
 *  Copyright (c) 2014-present, Facebook, Inc.
 *  All rights reserved.
 *
 *  This source code is licensed under the BSD-style license found in the
 *  LICENSE file in the root directory of this source tree. An additional grant
 *  of patent rights can be found in the PATENTS file in the same directory.
 *
 */

#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>

#import <ComponentKitTestHelpers/CKTestActionComponent.h>

#import <ComponentKit/CKBuildComponent.h>
#import <ComponentKit/CKAction.h>
#import <ComponentKit/CKCompositeComponent.h>
#import <ComponentKit/CKComponentSubclass.h>
#import <ComponentKit/CKComponentInternal.h>
#import <ComponentKit/CKComponentLayout.h>
#import <ComponentKit/CKComponentScopeRoot.h>
#import <ComponentKit/CKComponentScopeRootFactory.h>
#import <ComponentKit/CKComponentController.h>
#import <ComponentKit/CKThreadLocalComponentScope.h>

#import "CKComponentTestCase.h"

@interface CKComponentActionTestAssertionHandler : NSAssertionHandler
@end

@implementation CKComponentActionTestAssertionHandler

- (void)handleFailureInFunction:(NSString *)functionName
                           file:(NSString *)fileName
                     lineNumber:(NSInteger)line
                    description:(NSString *)format, ...
{}

@end

@interface CKTestScopeActionComponent : CKComponent

+ (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block;
+ (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block
        useComponentAsTarget:(BOOL)useComponentAsTarget;

- (void)triggerAction:(id)context;

@end

@implementation CKTestScopeActionComponent
{
  CKAction<id> _action;
  void (^_block)(CKComponent *, id);
}

+ (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block
{
  return [self newWithBlock:block useComponentAsTarget:NO];
}

+ (instancetype)newWithBlock:(void (^)(CKComponent *, id))block
        useComponentAsTarget:(BOOL)useComponentAsTarget
{
  CKComponentScope scope(self);

  CKTestScopeActionComponent *c = [super new];
  if (c) {
    if (useComponentAsTarget) {
      c->_action = {c, @selector(actionMethod:context:)};
    } else {
      c->_action = {scope, @selector(actionMethod:context:)};
    }
    c->_block = block;
  }
  return c;
}

- (void)actionMethod:(CKComponent *)sender context:(id)context
{
  _block(sender, context);
}

- (void)triggerAction:(id)context
{
  _action.send(self, context);
}

@end

@interface CKTestControllerScopeActionComponent : CKComponent

+ (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block;
+ (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block
        useComponentAsTarget:(BOOL)useComponentAsTarget;

- (void(^)(CKComponent *sender, id context))block;

- (void)triggerAction:(id)context;

@end

@interface CKTestControllerScopeActionComponentController : CKComponentController<CKTestControllerScopeActionComponent *>
@end

@implementation CKTestControllerScopeActionComponent
{
  CKAction<id> _action;
  void (^_block)(CKComponent *, id);
}

+ (instancetype)newWithBlock:(void(^)(CKComponent *sender, id context))block
{
  return [self newWithBlock:block useComponentAsTarget:NO];
}

+ (instancetype)newWithBlock:(void (^)(CKComponent *, id))block
        useComponentAsTarget:(BOOL)useComponentAsTarget
{
  CKComponentScope scope(self);

  CKTestControllerScopeActionComponent *c = [super new];
  if (c) {
    if (useComponentAsTarget) {
      c->_action = {c, @selector(actionMethod:context:)};
    } else {
      c->_action = {scope, @selector(actionMethod:context:)};
    }
    c->_block = block;
  }
  return c;
}

+ (Class<CKComponentControllerProtocol>)controllerClass
{
  return [CKTestControllerScopeActionComponentController class];
}

- (void (^)(CKComponent *, id))block
{
  return _block;
}

- (void)triggerAction:(id)context
{
  _action.send(self, context);
}

@end

@interface CKTestObjectTarget : NSObject

- (void)someMethod;

@property (nonatomic, assign, readonly) BOOL calledSomeMethod;

@end
@implementation CKTestObjectTarget

- (void)someMethod
{
  _calledSomeMethod = YES;
}

@end

@implementation CKTestControllerScopeActionComponentController
- (void)actionMethod:(CKComponent *)sender context:(id)context
{
  self.component.block(sender, context);
}
@end

@interface CKActionTests : CKComponentTestCase
@end

@implementation CKActionTests

- (void)testSendActionIncludesSenderComponent
{
  __block CKComponent *actionSender = nil;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ actionSender = sender; }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  CKActionSend(@selector(testAction:context:), innerComponent, nil);

  XCTAssert(actionSender == innerComponent, @"Sender should be inner component");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testSendActionIncludesContext
{
  __block id actionContext = nil;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ actionContext = context; }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  id context = @"context";

  CKActionSend(@selector(testAction:context:), innerComponent, context);

  XCTAssert(actionContext == context, @"Context should match what was passed to CKActionSend");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testSendActionIncludesMultipleContextObjects
{
  __block id actionContext = nil;
  __block id actionContext2 = nil;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { actionContext = obj1; actionContext2 = obj2; }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  id context = @"context";
  id context2 = @"context2";

  CKAction<id, id> action = { @selector(testAction2:context1:context2:) };
  action.send(innerComponent, context, context2);

  XCTAssert(actionContext == context && actionContext2 == context2, @"Contexts should match what was passed to CKActionSend");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testSendActionIncludingPrimitiveValue
{
  __block int actionInteger = 0;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { actionInteger = value; }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  int integer = 1337;

  CKAction<int> action = { @selector(testPrimitive:integer:) };
  action.send(innerComponent, integer);

  XCTAssert(actionInteger == integer, @"Contexts should match what was passed to CKActionSend");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testActionWithCppArgs
{
  __block std::vector<std::string> actionVec;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithCppArgumentBlock:^(CKComponent *sender, std::vector<std::string> vec) { actionVec = vec; }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  std::vector<std::string> cppThing = {"hummus", "chips", "salad"};
  CKAction<const std::vector<std::string> &> action = { @selector(testCppArgumentAction:vector:) };
  action.send(innerComponent, cppThing);

  XCTAssert(actionVec == cppThing, @"Contexts should match what was passed to CKActionSend");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testSendActionWithNoArguments
{
  __block BOOL calledNoArgumentBlock = NO;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { calledNoArgumentBlock = YES; }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  CKAction<> action = { @selector(testNoArgumentAction:) };
  action.send(innerComponent);

  XCTAssert(calledNoArgumentBlock, @"Contexts should match what was passed to CKActionSend");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testSendActionWithNoArgumentsWithAnActionThatExpectsObjectArguments
{
  __block BOOL calledNoArgumentBlock = NO;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { calledNoArgumentBlock = YES; }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  CKAction<id> action = { @selector(testNoArgumentAction:) };
  action.send(innerComponent, @"hello");

  XCTAssert(calledNoArgumentBlock, @"Contexts should match what was passed to CKActionSend");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

static CKAction<> createDemotedWithReference(void (^callback)(CKComponent*, int), int value) {
  int& ref = value;
  auto action = CKAction<int>::actionFromBlock(callback);
  auto demoted = CKAction<>::demotedFrom(action, ref);
  return demoted;
}

- (void)testSendActionWithObjectArgumentsWithDemotedActionWithoutArguments
{
  __block id actionContext = nil;
  __block id actionContext2 = nil;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { actionContext = obj1; actionContext2 = obj2; }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  id context = @"hello";
  id context2 = @"morty";

  CKAction<id, id> action = { @selector(testAction2:context1:context2:) };
  CKAction<> demotedAction = CKAction<>::demotedFrom(action, context, context2);
  demotedAction.send(innerComponent);

  __block int value;
  int expectedValue = 5;
  createDemotedWithReference(^(CKComponent *sender, int b) {
    value = b;
  }, expectedValue).send(innerComponent);

  XCTAssert(actionContext == context && actionContext2 == context2 && value == expectedValue, @"Contexts should match what was passed to CKActionSend");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testSendActionWithObjectArgumentsWithPromotedActionWithObjectArguments
{
  __block id actionContext = nil;
  __block id actionContext2 = nil;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ XCTFail(@"Should not be called."); }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { actionContext = obj1; actionContext2 = obj2; }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  id context = @"hello";
  id context2 = @"morty";

  CKAction<id, id> action = { @selector(testAction2:context1:context2:) };
  CKAction<id, id, id> promotedAction = CKAction<id, id>::promotedFrom<id>(action);
  promotedAction.send(innerComponent, context, context2, @"rick");

  XCTAssert(actionContext == context && actionContext2 == context2, @"Contexts should match what was passed to CKActionSend");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testSendActionStartingAtSenderNextResponderReachesParentComponent
{
  __block BOOL outerReceivedTestAction = NO;
  __block BOOL innerReceivedTestAction = NO;

  CKTestActionComponent *innerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ innerReceivedTestAction = YES; }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:[CKComponent new]];

  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ outerReceivedTestAction = YES; }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  CKActionSend(@selector(testAction:context:), innerComponent, nullptr);

  XCTAssertTrue(outerReceivedTestAction, @"Outer component should have received action sent by inner component");
  XCTAssertFalse(innerReceivedTestAction, @"Inner component should not have received action sent from it");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testSendActionStartingAtSenderDoesNotReachParentComponent
{
  __block BOOL outerReceivedTestAction = NO;
  __block BOOL innerReceivedTestAction = NO;

  CKTestActionComponent *innerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ innerReceivedTestAction = YES; }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:[CKComponent new]];

  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ outerReceivedTestAction = YES; }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  // Must be mounted to send actions:
  UIView *rootView = [UIView new];
  NSSet *mountedComponents = CKMountComponentLayout([outerComponent layoutThatFits:{} parentSize:{}], rootView, nil, nil);

  CKActionSend(@selector(testAction:context:), innerComponent, nil, CKActionSendBehaviorStartAtSender);

  XCTAssertFalse(outerReceivedTestAction, @"Outer component should not have received action since inner component did");
  XCTAssertTrue(innerReceivedTestAction, @"Inner component should have received action");

  [mountedComponents makeObjectsPerformSelector:@selector(unmount)];
}

- (void)testCombineTwoActionsWithNoArguments
{
  __block int action1CallCount = 0;
  __block int action2CallCount = 0;

  CKAction<> action1 = CKAction<>::actionFromBlock(^(CKComponent *) {
     XCTAssert(action1CallCount == 0, @"No actions should have been called yet");
     XCTAssert(action2CallCount == 0, @"No actions should have been called yet");
     action1CallCount++;
  });
  CKAction<> action2 = CKAction<>::actionFromBlock(^(CKComponent *) {
     XCTAssert(action1CallCount == 1, @"First action should be called before the second action");
     XCTAssert(action2CallCount == 0, @"Second action shouldn't have been called yet");
     action2CallCount++;
  });

  CKAction<>::combine(action1, action2).send([CKComponent new]);

  XCTAssert(action1CallCount == 1, @"First action should have been called once");
  XCTAssert(action2CallCount == 1, @"Second action should habe been called once");
}

- (void)testCombineThreeActionsWithNoArguments
{
  __block int action1CallCount = 0;
  __block int action2CallCount = 0;
  __block int action3CallCount = 0;

  CKAction<> action1 = CKAction<>::actionFromBlock(^(CKComponent *) {
     XCTAssert(action1CallCount == 0, @"No actions should have been called yet");
     XCTAssert(action2CallCount == 0, @"No actions should have been called yet");
     XCTAssert(action3CallCount == 0, @"No actions should have been called yet");
     action1CallCount++;
  });
  CKAction<> action2 = CKAction<>::actionFromBlock(^(CKComponent *) {
     XCTAssert(action1CallCount == 1, @"First action should be called before the second action");
     XCTAssert(action2CallCount == 0, @"Second action shouldn't have been called yet");
     XCTAssert(action3CallCount == 0, @"Third action shouldn't have been called yet");
     action2CallCount++;
  });
  CKAction<> action3 = CKAction<>::actionFromBlock(^(CKComponent *) {
     XCTAssert(action1CallCount == 1, @"First action should be called before the second action");
     XCTAssert(action2CallCount == 1, @"Second action should be called before the third action");
     XCTAssert(action3CallCount == 0, @"Third action shouldn't have been called yet");
     action3CallCount++;
  });

  CKAction<>::combine(action1, action2, action3).send([CKComponent new]);

  XCTAssert(action1CallCount == 1, @"First action should have been called once");
  XCTAssert(action2CallCount == 1, @"Second action should have been called once");
  XCTAssert(action3CallCount == 1, @"Third action should have been called once");
}

- (void)testTargetSelectorActionCallsOnTargetWithoutMounting
{
  __block BOOL calledBlock = NO;

  CKComponent *innerComponent = [CKComponent new];
  CKTestActionComponent *outerComponent =
  [CKTestActionComponent
   newWithSingleArgumentBlock:^(CKComponent *sender, id context){ calledBlock = YES; }
   secondArgumentBlock:^(CKComponent *sender, id obj1, id obj2) { XCTFail(@"Should not be called."); }
   primitiveArgumentBlock:^(CKComponent *sender, int value) { XCTFail(@"Should not be called."); }
   noArgumentBlock:^(CKComponent *sender) { XCTFail(@"Should not be called."); }
   component:innerComponent];

  CKAction<id> action { outerComponent, @selector(testAction:context:) };
  action.send(innerComponent, CKActionSendBehaviorStartAtSender, @"hello");

  XCTAssertTrue(calledBlock, @"Outer component should have received the action, even though the components are not mounted.");
}

- (void)testScopeActionCallsMethodOnScopedComponent
{
  __block BOOL calledAction = NO;

  // We have to use build component here to ensure the scopes are properly configured.
  CKTestScopeActionComponent *component = (CKTestScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
    return [CKTestScopeActionComponent
            newWithBlock:^(CKComponent *sender, id context) {
              calledAction = YES;
            }];
  }).component;

  [component triggerAction:nil];

  XCTAssertTrue(calledAction, @"Should have called the action on the test component");
}

- (void)testComponentAsTargetActionCallsMethodOnComponent
{
  __block BOOL calledAction = NO;

  // We have to use build component here to ensure the scopes are properly configured.
  CKTestScopeActionComponent *component = (CKTestScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
    return [CKTestScopeActionComponent
            newWithBlock:^(CKComponent *sender, id context) {
              calledAction = YES;
            } useComponentAsTarget:YES];
  }).component;

  [component triggerAction:nil];

  XCTAssertTrue(calledAction, @"Should have called the action on the test component");
}

- (void)testScopeActionCallsMethodOnScopedComponentWithCorrectContext
{
  __block id actionContext = nil;

  // We have to use build component here to ensure the scopes are properly configured.
  CKTestScopeActionComponent *component = (CKTestScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
    return [CKTestScopeActionComponent
            newWithBlock:^(CKComponent *sender, id context) {
              actionContext = context;
            }];
  }).component;

  id context = @"hello";

  [component triggerAction:context];

  XCTAssertTrue(actionContext == context, @"Context should have been passed to scope component action call");
}

- (void)testScopeActionCallsMethodOnScopedComponentControllerIfNotImplementedOnComponent
{
  __block BOOL calledAction = NO;

  // We have to use build component here to ensure the scopes are properly configured.
  CKTestControllerScopeActionComponent *component = (CKTestControllerScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
    return [CKTestControllerScopeActionComponent
            newWithBlock:^(CKComponent *sender, id context) {
              calledAction = YES;
            }];
  }).component;

  [component triggerAction:nil];

  XCTAssertTrue(calledAction, @"Should have called the action on the test component");
}

- (void)testComponentAsTargetActionCallsMethodOnComponentControllerIfNotImplementedOnComponent
{
  __block BOOL calledAction = NO;

  // We have to use build component here to ensure the scopes are properly configured.
  CKTestControllerScopeActionComponent *component = (CKTestControllerScopeActionComponent *)CKBuildComponent(CKComponentScopeRootWithDefaultPredicates(nil, nil), {}, ^{
    return [CKTestControllerScopeActionComponent
            newWithBlock:^(CKComponent *sender, id context) {
              calledAction = YES;
            } useComponentAsTarget:YES];
  }).component;

  [component triggerAction:nil];

  XCTAssertTrue(calledAction, @"Should have called the action on the test component");
}

- (void)testTargetSelectorActionCallsOnNormalNSObject
{
  CKTestObjectTarget *target = [CKTestObjectTarget new];
  CKAction<> action = {target, @selector(someMethod)};
  action.send([CKComponent new]);

  XCTAssertTrue(target.calledSomeMethod, @"Should have called the method on target");
}

- (void)testImpIsNilWhenSelectorIsNil
{
  XCTAssert(!CKActionFind(nil, nil).imp);
}

- (void)testImpIsNilWhenTargetIsNil
{
  XCTAssert(!CKActionFind(@selector(triggerAction:), nil).imp);
}

- (void)testResponderIsNilWhenSelectorIsNil
{
  XCTAssertNil(CKActionFind(nil, nil).responder);
}

- (void)testResponderIsNilWhenTargetIsNil
{
  XCTAssertNil(CKActionFind(@selector(triggerAction:), nil).responder);
}

- (void)testBlockActionFires
{
  __block BOOL firedAction = NO;
  CKAction<> action = CKAction<>::actionFromBlock(^(CKComponent *) {
    firedAction = YES;
  });

  action.send([CKComponent new]);

  XCTAssertTrue(firedAction);
}

- (void)testBlockActionFiresAndDeliversComponentAsSender
{
  __block BOOL equalComponents = NO;
  CKComponent *c = [CKComponent new];
  CKAction<> action = CKAction<>::actionFromBlock(^(CKComponent *passedComponent) {
    equalComponents = (passedComponent == c);
  });

  action.send(c);

  XCTAssertTrue(equalComponents);
}

- (void)testBlockActionFiresAndDeliversAdditionalParameterAsArgument
{
  __block BOOL equalArguments = NO;
  NSObject *arg = [NSObject new];
  CKAction<NSObject *> action = CKAction<NSObject *>::actionFromBlock(^(CKComponent *c, NSObject *passedArgument) {
    equalArguments = (passedArgument == arg);
  });

  action.send([CKComponent new], arg);
  XCTAssertTrue(equalArguments);
}

- (void)testThatScopeActionWithSameSelectorHaveUniqueIdentifiers
{
  CKThreadLocalComponentScope threadScope(CKComponentScopeRootWithDefaultPredicates(nil, nil), {});

  CKComponentScope scope([CKTestScopeActionComponent class], @"moose");
  const CKAction<> action1 = {scope, @selector(triggerAction:)};

  CKComponentScope scope2([CKTestScopeActionComponent class], @"cat");
  const CKAction<> action2 = {scope2, @selector(triggerAction:)};

  XCTAssertNotEqual(action1.identifier(), action2.identifier());
}

- (void)testThatBlockActionsWithDistinctBlocksHaveUniqueIdentifiers
{
  const CKAction<> action1 = CKAction<>::actionFromBlock(^(CKComponent *sender){
    exit(1);
  });
  const CKAction<> action2 = CKAction<>::actionFromBlock(^(CKComponent *sender){
    exit(2);
  });
  XCTAssertNotEqual(action1.identifier(), action2.identifier());
}

#pragma mark - Action Params Validation

- (BOOL)checkSelector:(SEL)sel typeEncodings:(const std::vector<const char *> &)typeEncodings {
  Method method = class_getInstanceMethod([self class], sel);
  return checkMethodSignatureAgainstTypeEncodings(sel, method, typeEncodings);
}

- (void)testActionNoParamValidation
{
  const SEL selector = @selector(triggerActionWithComponent:);
  std::vector<const char *> encodings;
  CKActionTypeVectorBuild(encodings, CKActionTypelist<>{});
  XCTAssertTrue(([self checkSelector:selector typeEncodings:encodings]));
}

- (void)triggerActionWithComponent:(id)sender {}

- (void)testActionPrimitiveParamValidation
{
  const SEL selector = @selector(triggerActionWithComponent:value:);
  std::vector<const char *> encodings;
  CKActionTypeVectorBuild(encodings, CKActionTypelist<int>{});
  XCTAssertTrue(([self checkSelector:selector typeEncodings:encodings]));
}

- (void)triggerActionWithComponent:(id)sender value:(int)val {}

- (void)testActionObjectAndPrimitiveParamValidation
{
  const SEL selector = @selector(triggerActionWithComponent:value:value:);
  std::vector<const char *> encodings;
  CKActionTypeVectorBuild(encodings, CKActionTypelist<NSString *, char>{});
  XCTAssertTrue(([self checkSelector:selector typeEncodings:encodings]));
}

- (void)triggerActionWithComponent:(id)sender value:(NSString *)obj value:(char)val {}

- (void)testActionCppParamsValidation
{
  const SEL selector = @selector(triggerActionWithComponent:vector:constVector:constValVector:vectorRef:vectorRval:);
  std::vector<const char *> encodings;
  CKActionTypeVectorBuild(encodings,
                          CKActionTypelist<
                          std::vector<int>,
                          const std::vector<int>,
                          std::vector<const int>,
                          std::vector<int> &,
                          std::vector<int> &&
                          >{});
  XCTAssertTrue(([self checkSelector:selector typeEncodings:encodings]));
}

- (void)triggerActionWithComponent:(id)sender
                            vector:(std::vector<int>)val
                       constVector:(const std::vector<int>)conVal
                    constValVector:(std::vector<const int>)conValVec
                         vectorRef:(std::vector<int> &)vecRef
                        vectorRval:(std::vector<int> &&)vecRval {}

- (void)testActionParamsFailedValidation
{
  // We need to set an assertion handler as `checkMethodSignatureAgainstTypeEncodings` throws `RCCFailAssert` in case it fails.
  auto const assertionHandler = [CKComponentActionTestAssertionHandler new];
  [[[NSThread currentThread] threadDictionary] setValue:assertionHandler forKey:NSAssertionHandlerKey];

  const SEL selector = @selector(triggerActionWithComponent:vector:object:primitive:);
  std::vector<const char *> encodings;

  // wrong c++ type
  CKActionTypeVectorBuild(encodings, CKActionTypelist<std::vector<NSURL *>, NSObject *, BOOL>{});
  XCTAssertFalse(([self checkSelector:selector typeEncodings:encodings]));

  // wrong object
  CKActionTypeVectorBuild(encodings, CKActionTypelist<std::vector<int>, NSInteger, BOOL>{});
  XCTAssertFalse(([self checkSelector:selector typeEncodings:encodings]));

  // wrong primitive
  CKActionTypeVectorBuild(encodings, CKActionTypelist<std::vector<int>, NSObject *, char >{});
  XCTAssertFalse(([self checkSelector:selector typeEncodings:encodings]));
}

- (void)triggerActionWithComponent:(id)sender
                            vector:(std::vector<int>)val
                            object:(NSObject *)conVal
                         primitive:(BOOL)prim {}

#pragma mark - Equality.

- (void)testRawSelectorEquality
{
  const SEL selector = @selector(triggerAction:);
  const CKUntypedComponentAction action1 = {selector};
  const CKUntypedComponentAction action2 = {selector};
  XCTAssertTrue(action1 == action2);

  const CKUntypedComponentAction unequalAction = {@selector(stringWithFormat:)};
  XCTAssertFalse(action1 == unequalAction);
}

- (void)testTargetSelectorActionEquality
{
  NSMutableArray *const target = [NSMutableArray new];
  const SEL selector = @selector(removeLastObject);
  const CKAction<> action1 = {target, selector};
  const CKAction<> action2 = {target, selector};
  XCTAssertTrue(action1 == action2);

  const CKAction<> actionWithUnequalTarget = {[NSMutableArray new], selector};
  XCTAssertFalse(action1 == actionWithUnequalTarget);

  const CKAction<> actionWithUnequalSelector = {target, @selector(removeAllObjects)};
  XCTAssertFalse(action1 == actionWithUnequalSelector);
}

- (void)testBlockActionEquality
{
  void (^block)(CKComponent *c, NSObject *passedArgument) {};
  const CKAction<NSObject *> action = CKAction<NSObject *>::actionFromBlock(block);
  XCTAssertTrue(action == CKAction<NSObject *>::actionFromBlock(block));
  XCTAssertFalse(action == CKAction<NSObject *>::actionFromBlock(^(CKComponent *, NSObject *__strong) {}));
}

- (void)testScopedActionEquality
{
  CKThreadLocalComponentScope threadScope(CKComponentScopeRootWithDefaultPredicates(nil, nil), {});

  const SEL selector = @selector(triggerAction:);
  CKComponentScope scope([CKTestScopeActionComponent class], @"Marty McFly");
  const CKAction<> action1 = {scope, selector};
  const CKAction<> action2 = {scope, selector};
  XCTAssertTrue(action1 == action2);

  CKComponentScope scope2([CKTestScopeActionComponent class], @"Biff Tannon");
  const CKAction<> unequalAction = {scope2, selector};
  XCTAssertFalse(action1 == unequalAction);
}

@end
