/**
 * Copyright (c) 2016-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.
 */

#import <XCTest/XCTest.h>

#import <FBRetainCycleDetector/FBObjectiveCGraphElement+Internal.h>
#import <FBRetainCycleDetector/FBObjectiveCObject.h>
#import <FBRetainCycleDetector/FBRetainCycleDetector.h>

@interface _RCDObjectWrapperTestClass : NSObject
- (instancetype)initWithOtherObject:(_RCDObjectWrapperTestClass *)object;
@property (nonatomic, strong) NSObject *someObject;
@property (nonatomic, copy) NSString *someString;
@property (nonatomic, weak) NSObject *irrelevantObject;
@property (nonatomic, strong) id aCls;
@end
@implementation _RCDObjectWrapperTestClass
{
  _RCDObjectWrapperTestClass *_someTestClassInstance;
}

- (instancetype)initWithOtherObject:(_RCDObjectWrapperTestClass *)object
{
  if (self = [super init]) {
    _someTestClassInstance = object;
  }

  return self;
}

@end

@interface _RCDObjectWrapperTestClassSubclass : _RCDObjectWrapperTestClass
@end
@implementation _RCDObjectWrapperTestClassSubclass
@end

@interface FBObjectiveCObjectTests : XCTestCase
@end
@implementation FBObjectiveCObjectTests

#if _INTERNAL_RCD_ENABLED

- (void)testObjectsRetainedBySomeObjectWillBeFetched
{
  NSObject *someObject = [NSObject new];
  NSString *someString = @"someString";
  NSObject *irrelevant = [NSObject new];
  _RCDObjectWrapperTestClass *verifyObject = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *testObject = [[_RCDObjectWrapperTestClass alloc] initWithOtherObject:verifyObject];
  testObject.someObject = someObject;
  testObject.someString = someString;
  testObject.irrelevantObject = irrelevant;

  FBObjectiveCObject *object = [[FBObjectiveCObject alloc] initWithObject:testObject];
  NSSet *retainedObjects = [object allRetainedObjects];

  XCTAssertFalse([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:irrelevant]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someObject]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someString]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:verifyObject]]);

}

- (void)testObjectsRetainedByArrayWillBeFetched
{
  NSString *someString = @"someString";
  NSObject *someObject = [NSObject new];
  NSDictionary *someDictionary = [NSDictionary new];
  NSArray *testedArray = @[someString, someObject, someDictionary];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:testedArray];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someString]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someObject]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someDictionary]]);
}

- (void)testObjectsRetainedByDictionaryWillBeFetched
{
  NSString *someString = @"someString";
  NSObject *someObject = [NSObject new];

  NSDictionary *someDictionary = @{someString:someObject};

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:someDictionary];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someString]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someObject]]);
}

- (void)testObjectsRetainedBySetWillBeFetched
{
  NSString *someString = @"someString";
  NSObject *someObject = [NSObject new];

  NSSet *someSet = [NSSet setWithObjects:someString, someObject, nil];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:someSet];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someString]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someObject]]);
}

- (void)testThatIfObjectHasStrongPropertyWithNilThenItWontFetchIt
{
  _RCDObjectWrapperTestClass *someObject = [_RCDObjectWrapperTestClass new];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:someObject];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertEqual([retainedObjects count], 0);
}

- (void)testObjectThatSubclassesFromObjectWithStrongPropertiesWillFetchPropertiesFromParentClass
{
  _RCDObjectWrapperTestClassSubclass *testObject = [_RCDObjectWrapperTestClassSubclass new];
  NSObject *someObject = [NSObject new];
  NSObject *irrelevantObject = [NSObject new];
  NSString *someString = @"someString";
  testObject.someObject = someObject;
  testObject.irrelevantObject = irrelevantObject;
  testObject.someString = someString;

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:testObject];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someString]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someObject]]);
  XCTAssertFalse([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:irrelevantObject]]);
}

- (void)testObjectRetainingClassConformingToFastEnumerationWillNotCrash
{
  _RCDObjectWrapperTestClass *someObject = [_RCDObjectWrapperTestClass new];
  someObject.aCls = [NSArray class];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:someObject];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:[NSArray class]]]);
}

- (void)testHashTableWithWeakObjectsWillNotFetchThoseObjects
{
  NSHashTable *hashTable = [NSHashTable weakObjectsHashTable];

  _RCDObjectWrapperTestClass *someObject1 = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *someObject2 = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *someObject3 = [_RCDObjectWrapperTestClass new];

  [hashTable addObject:someObject1];
  [hashTable addObject:someObject2];
  [hashTable addObject:someObject3];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:hashTable];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertEqual([retainedObjects count], 0);
}

- (void)testHashTableWithStrongObjectsWillFetchThoseObjects
{
  NSHashTable *hashTable = [NSHashTable new];

  _RCDObjectWrapperTestClass *someObject1 = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *someObject2 = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *someObject3 = [_RCDObjectWrapperTestClass new];

  [hashTable addObject:someObject1];
  [hashTable addObject:someObject2];
  [hashTable addObject:someObject3];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:hashTable];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someObject1]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someObject2]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:someObject3]]);
}

- (void)testMapTableWithWeakKeysAndValueWillNotFetchAnything
{
  NSMapTable *mapTable = [NSMapTable weakToWeakObjectsMapTable];

  _RCDObjectWrapperTestClass *keyObject = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *valueObject = [_RCDObjectWrapperTestClass new];

  [mapTable setObject:valueObject forKey:keyObject];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:mapTable];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertEqual([retainedObjects count], 0);
}

- (void)testMapTableWithWeakKeysAndStrongValuesWillFetchOnlyValues
{
  NSMapTable *mapTable = [NSMapTable weakToStrongObjectsMapTable];

  _RCDObjectWrapperTestClass *keyObject = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *valueObject = [_RCDObjectWrapperTestClass new];

  [mapTable setObject:valueObject forKey:keyObject];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:mapTable];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertEqual([retainedObjects count], 1);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:valueObject]]);
}

- (void)testMapTableWithStrongKeysAndWeakValuesWillFetchOnlyKeys
{
  NSMapTable *mapTable = [NSMapTable strongToWeakObjectsMapTable];

  _RCDObjectWrapperTestClass *keyObject = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *valueObject = [_RCDObjectWrapperTestClass new];

  [mapTable setObject:valueObject forKey:keyObject];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:mapTable];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertEqual([retainedObjects count], 1);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:keyObject]]);
}

- (void)testMapTableWithStrongKeysAndStrongValuesWillFetchBothKeysAndValues
{
  NSMapTable *mapTable = [NSMapTable strongToStrongObjectsMapTable];

  _RCDObjectWrapperTestClass *keyObject = [_RCDObjectWrapperTestClass new];
  _RCDObjectWrapperTestClass *valueObject = [_RCDObjectWrapperTestClass new];

  [mapTable setObject:valueObject forKey:keyObject];

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:mapTable];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertEqual([retainedObjects count], 2);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:keyObject]]);
  XCTAssertTrue([retainedObjects containsObject:[[FBObjectiveCObject alloc] initWithObject:valueObject]]);
}

- (void)testTollFreeBridgedDictionaryWillNotCrash
{
  CFDictionaryValueCallBacks cb = kCFTypeDictionaryValueCallBacks;
  cb.retain = NULL;
  cb.release = NULL;
  NSMutableDictionary *dictionary = (__bridge_transfer id)CFDictionaryCreateMutable(NULL, 0, NULL, &cb);
  NSInteger intV = 5;
  CFDictionarySetValue((CFMutableDictionaryRef)dictionary, (__bridge const void *)@"key", (const void *)intV);

  FBObjectiveCObject *abstractedObject = [[FBObjectiveCObject alloc] initWithObject:dictionary];

  NSSet *retainedObjects = [abstractedObject allRetainedObjects];

  XCTAssertEqual([retainedObjects count], 0);
}

#endif //_INTERNAL_RCD_ENABLED

@end
