//
//  RACKVOWrapperSpec.m
//  ReactiveCocoa
//
//  Created by Justin Spahr-Summers on 2012-08-07.
//  Copyright (c) 2012 GitHub, Inc. All rights reserved.
//

#import "NSObject+RACKVOWrapper.h"

#import "EXTKeyPathCoding.h"
#import "NSObject+RACDeallocating.h"
#import "RACCompoundDisposable.h"
#import "RACDisposable.h"
#import "RACKVOTrampoline.h"
#import "RACTestObject.h"

@interface RACTestOperation : NSOperation
@end

// The name of the examples.
static NSString * const RACKVOWrapperExamples = @"RACKVOWrapperExamples";

// A block that returns an object to observe in the examples.
static NSString * const RACKVOWrapperExamplesTargetBlock = @"RACKVOWrapperExamplesTargetBlock";

// The key path to observe in the examples.
//
// The key path must have at least one weak property in it.
static NSString * const RACKVOWrapperExamplesKeyPath = @"RACKVOWrapperExamplesKeyPath";

// A block that changes the value of a weak property in the observed key path.
// The block is passed the object the example is observing and the new value the
// weak property should be changed to.
static NSString * const RACKVOWrapperExamplesChangeBlock = @"RACKVOWrapperExamplesChangeBlock";

// A block that returns a valid value for the weak property changed by
// RACKVOWrapperExamplesChangeBlock. The value must deallocate
// normally.
static NSString * const RACKVOWrapperExamplesValueBlock = @"RACKVOWrapperExamplesValueBlock";

// Whether RACKVOWrapperExamplesChangeBlock changes the value
// of the last key path component in the key path directly.
static NSString * const RACKVOWrapperExamplesChangesValueDirectly = @"RACKVOWrapperExamplesChangesValueDirectly";

// The name of the examples.
static NSString * const RACKVOWrapperCollectionExamples = @"RACKVOWrapperCollectionExamples";

// A block that returns an object to observe in the examples.
static NSString * const RACKVOWrapperCollectionExamplesTargetBlock = @"RACKVOWrapperCollectionExamplesTargetBlock";

// The key path to observe in the examples.
//
// Must identify a property of type NSOrderedSet.
static NSString * const RACKVOWrapperCollectionExamplesKeyPath = @"RACKVOWrapperCollectionExamplesKeyPath";

SharedExampleGroupsBegin(RACKVOWrapperExamples)

sharedExamplesFor(RACKVOWrapperExamples, ^(NSDictionary *data) {
	__block NSObject *target = nil;
	__block NSString *keyPath = nil;
	__block void (^changeBlock)(NSObject *, id) = nil;
	__block id (^valueBlock)(void) = nil;
	__block BOOL changesValueDirectly = NO;

	__block NSUInteger priorCallCount = 0;
	__block NSUInteger posteriorCallCount = 0;
	__block BOOL priorTriggeredByLastKeyPathComponent = NO;
	__block BOOL posteriorTriggeredByLastKeyPathComponent = NO;
	__block BOOL posteriorTriggeredByDeallocation = NO;
	__block void (^callbackBlock)(id, NSDictionary *) = nil;

	beforeEach(^{
		NSObject * (^targetBlock)(void) = data[RACKVOWrapperExamplesTargetBlock];
		target = targetBlock();
		keyPath = data[RACKVOWrapperExamplesKeyPath];
		changeBlock = data[RACKVOWrapperExamplesChangeBlock];
		valueBlock = data[RACKVOWrapperExamplesValueBlock];
		changesValueDirectly = [data[RACKVOWrapperExamplesChangesValueDirectly] boolValue];

		priorCallCount = 0;
		posteriorCallCount = 0;

		callbackBlock = [^(id value, NSDictionary *change) {
			if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
				priorTriggeredByLastKeyPathComponent = [change[RACKeyValueChangeAffectedOnlyLastComponentKey] boolValue];
				++priorCallCount;
				return;
			}
			posteriorTriggeredByLastKeyPathComponent = [change[RACKeyValueChangeAffectedOnlyLastComponentKey] boolValue];
			posteriorTriggeredByDeallocation = [change[RACKeyValueChangeCausedByDeallocationKey] boolValue];
			++posteriorCallCount;
		} copy];
	});

	afterEach(^{
		target = nil;
		keyPath = nil;
		changeBlock = nil;
		valueBlock = nil;
		changesValueDirectly = NO;

		callbackBlock = nil;
	});

	it(@"should not call the callback block on add if called without NSKeyValueObservingOptionInitial", ^{
		[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
		expect(priorCallCount).to.equal(0);
		expect(posteriorCallCount).to.equal(0);
	});

	it(@"should call the callback block on add if called with NSKeyValueObservingOptionInitial", ^{
		[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior | NSKeyValueObservingOptionInitial observer:nil block:callbackBlock];
		expect(priorCallCount).to.equal(0);
		expect(posteriorCallCount).to.equal(1);
	});

	it(@"should call the callback block twice per change, once prior and once posterior", ^{
		[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
		priorCallCount = 0;
		posteriorCallCount = 0;

		id value1 = valueBlock();
		changeBlock(target, value1);
		expect(priorCallCount).to.equal(1);
		expect(posteriorCallCount).to.equal(1);
		expect(priorTriggeredByLastKeyPathComponent).to.equal(changesValueDirectly);
		expect(posteriorTriggeredByLastKeyPathComponent).to.equal(changesValueDirectly);
		expect(posteriorTriggeredByDeallocation).to.beFalsy();

		id value2 = valueBlock();
		changeBlock(target, value2);
		expect(priorCallCount).to.equal(2);
		expect(posteriorCallCount).to.equal(2);
		expect(priorTriggeredByLastKeyPathComponent).to.equal(changesValueDirectly);
		expect(posteriorTriggeredByLastKeyPathComponent).to.equal(changesValueDirectly);
		expect(posteriorTriggeredByDeallocation).to.beFalsy();
	});

	it(@"should call the callback block with NSKeyValueChangeNotificationIsPriorKey set before the value is changed, and not set after the value is changed", ^{
		__block BOOL priorCalled = NO;
		__block BOOL posteriorCalled = NO;
		__block id priorValue = nil;
		__block id posteriorValue = nil;

		id value1 = valueBlock();
		changeBlock(target, value1);
		id oldValue = [target valueForKeyPath:keyPath];

		[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:^(id value, NSDictionary *change) {
			if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
				priorCalled = YES;
				priorValue = value;
				expect(posteriorCalled).to.beFalsy();
				return;
			}
			posteriorCalled = YES;
			posteriorValue = value;
			expect(priorCalled).to.beTruthy();
		}];

		id value2 = valueBlock();
		changeBlock(target, value2);
		id newValue = [target valueForKeyPath:keyPath];
		expect(priorCalled).to.beTruthy();
		expect(priorValue).to.equal(oldValue);
		expect(posteriorCalled).to.beTruthy();
		expect(posteriorValue).to.equal(newValue);
	});

	it(@"should not call the callback block after it's been disposed", ^{
		RACDisposable *disposable = [target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
		priorCallCount = 0;
		posteriorCallCount = 0;

		[disposable dispose];
		expect(priorCallCount).to.equal(0);
		expect(posteriorCallCount).to.equal(0);

		id value = valueBlock();
		changeBlock(target, value);
		expect(priorCallCount).to.equal(0);
		expect(posteriorCallCount).to.equal(0);
	});

	it(@"should call the callback block only once with NSKeyValueChangeNotificationIsPriorKey not set when the value is deallocated", ^{
		__block BOOL valueDidDealloc = NO;

		[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];

		@autoreleasepool {
			NSObject *value __attribute__((objc_precise_lifetime)) = valueBlock();
			[value.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
				valueDidDealloc = YES;
			}]];

			changeBlock(target, value);
			priorCallCount = 0;
			posteriorCallCount = 0;
		}

		expect(valueDidDealloc).to.beTruthy();
		expect(priorCallCount).to.equal(0);
		expect(posteriorCallCount).to.equal(1);
		expect(posteriorTriggeredByDeallocation).to.beTruthy();
	});
});

sharedExamplesFor(RACKVOWrapperCollectionExamples, ^(NSDictionary *data) {
	__block NSObject *target = nil;
	__block NSString *keyPath = nil;
	__block NSMutableOrderedSet *mutableKeyPathProxy = nil;
	__block void (^callbackBlock)(id, NSDictionary *) = nil;

	__block id priorValue = nil;
	__block id posteriorValue = nil;
	__block NSDictionary *priorChange = nil;
	__block NSDictionary *posteriorChange = nil;

	beforeEach(^{
		NSObject * (^targetBlock)(void) = data[RACKVOWrapperCollectionExamplesTargetBlock];
		target = targetBlock();
		keyPath = data[RACKVOWrapperCollectionExamplesKeyPath];

		callbackBlock = [^(id value, NSDictionary *change) {
			if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
				priorValue = value;
				priorChange = change;
				return;
			}
			posteriorValue = value;
			posteriorChange = change;
		} copy];

		[target setValue:[NSOrderedSet orderedSetWithObject:@0] forKeyPath:keyPath];
		[target rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionPrior observer:nil block:callbackBlock];
		mutableKeyPathProxy = [target mutableOrderedSetValueForKeyPath:keyPath];
	});

	afterEach(^{
		target = nil;
		keyPath = nil;
		callbackBlock = nil;

		priorValue = nil;
		priorChange = nil;
		posteriorValue = nil;
		posteriorChange = nil;
	});

	it(@"should support inserting elements into ordered collections", ^{
		[mutableKeyPathProxy insertObject:@1 atIndex:0];

		expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
		expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:(@[ @1, @0 ])]);
		expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeInsertion);
		expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeInsertion);
		expect(priorChange[NSKeyValueChangeOldKey]).to.beNil();
		expect(posteriorChange[NSKeyValueChangeNewKey]).to.equal(@[ @1 ]);
		expect(priorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
		expect(posteriorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
	});

	it(@"should support removing elements from ordered collections", ^{
		[mutableKeyPathProxy removeObjectAtIndex:0];

		expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
		expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:@[]]);
		expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeRemoval);
		expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeRemoval);
		expect(priorChange[NSKeyValueChangeOldKey]).to.equal(@[ @0 ]);
		expect(posteriorChange[NSKeyValueChangeNewKey]).to.beNil();
		expect(priorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
		expect(posteriorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
	});

	it(@"should support replacing elements in ordered collections", ^{
		[mutableKeyPathProxy replaceObjectAtIndex:0 withObject:@1];

		expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
		expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @1 ]]);
		expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeReplacement);
		expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeReplacement);
		expect(priorChange[NSKeyValueChangeOldKey]).to.equal(@[ @0 ]);
		expect(posteriorChange[NSKeyValueChangeNewKey]).to.equal(@[ @1 ]);
		expect(priorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
		expect(posteriorChange[NSKeyValueChangeIndexesKey]).to.equal([NSIndexSet indexSetWithIndex:0]);
	});

	it(@"should support adding elements to unordered collections", ^{
		[mutableKeyPathProxy unionOrderedSet:[NSOrderedSet orderedSetWithObject:@1]];

		expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
		expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:(@[ @0, @1 ])]);
		expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeInsertion);
		expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeInsertion);
		expect(priorChange[NSKeyValueChangeOldKey]).to.beNil();
		expect(posteriorChange[NSKeyValueChangeNewKey]).to.equal(@[ @1 ]);
	});

	it(@"should support removing elements from unordered collections", ^{
		[mutableKeyPathProxy minusOrderedSet:[NSOrderedSet orderedSetWithObject:@0]];

		expect(priorValue).to.equal([NSOrderedSet orderedSetWithArray:@[ @0 ]]);
		expect(posteriorValue).to.equal([NSOrderedSet orderedSetWithArray:@[]]);
		expect(priorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeRemoval);
		expect(posteriorChange[NSKeyValueChangeKindKey]).to.equal(NSKeyValueChangeRemoval);
		expect(priorChange[NSKeyValueChangeOldKey]).to.equal(@[ @0 ]);
		expect(posteriorChange[NSKeyValueChangeNewKey]).to.beNil();
	});
});

SharedExampleGroupsEnd

SpecBegin(RACKVOWrapper)

describe(@"-rac_observeKeyPath:options:observer:block:", ^{
	describe(@"on simple keys", ^{
		NSObject * (^targetBlock)(void) = ^{
			return [[RACTestObject alloc] init];
		};

		void (^changeBlock)(RACTestObject *, id) = ^(RACTestObject *target, id value) {
			target.weakTestObjectValue = value;
		};

		id (^valueBlock)(void) = ^{
			return [[RACTestObject alloc] init];
		};

		itShouldBehaveLike(RACKVOWrapperExamples, @{
			RACKVOWrapperExamplesTargetBlock: targetBlock,
			RACKVOWrapperExamplesKeyPath: @keypath(RACTestObject.new, weakTestObjectValue),
			RACKVOWrapperExamplesChangeBlock: changeBlock,
			RACKVOWrapperExamplesValueBlock: valueBlock,
			RACKVOWrapperExamplesChangesValueDirectly: @YES
		});

		itShouldBehaveLike(RACKVOWrapperCollectionExamples, @{
			RACKVOWrapperCollectionExamplesTargetBlock: targetBlock,
			RACKVOWrapperCollectionExamplesKeyPath: @keypath(RACTestObject.new, orderedSetValue)
		});
	});

	describe(@"on composite key paths'", ^{
		describe(@"last key path components", ^{
			NSObject *(^targetBlock)(void) = ^{
				RACTestObject *object = [[RACTestObject alloc] init];
				object.strongTestObjectValue = [[RACTestObject alloc] init];
				return object;
			};

			void (^changeBlock)(RACTestObject *, id) = ^(RACTestObject *target, id value) {
				target.strongTestObjectValue.weakTestObjectValue = value;
			};

			id (^valueBlock)(void) = ^{
				return [[RACTestObject alloc] init];
			};

			itShouldBehaveLike(RACKVOWrapperExamples, @{
				RACKVOWrapperExamplesTargetBlock: targetBlock,
				RACKVOWrapperExamplesKeyPath: @keypath(RACTestObject.new, strongTestObjectValue.weakTestObjectValue),
				RACKVOWrapperExamplesChangeBlock: changeBlock,
				RACKVOWrapperExamplesValueBlock: valueBlock,
				RACKVOWrapperExamplesChangesValueDirectly: @YES
			});

			itShouldBehaveLike(RACKVOWrapperCollectionExamples, @{
				RACKVOWrapperCollectionExamplesTargetBlock: targetBlock,
				RACKVOWrapperCollectionExamplesKeyPath: @keypath(RACTestObject.new, strongTestObjectValue.orderedSetValue)
			});
		});

		describe(@"intermediate key path components", ^{
			NSObject *(^targetBlock)(void) = ^{
				return [[RACTestObject alloc] init];
			};

			void (^changeBlock)(RACTestObject *, id) = ^(RACTestObject *target, id value) {
				target.weakTestObjectValue = value;
			};

			id (^valueBlock)(void) = ^{
				RACTestObject *object = [[RACTestObject alloc] init];
				object.strongTestObjectValue = [[RACTestObject alloc] init];
				return object;
			};

			itShouldBehaveLike(RACKVOWrapperExamples, @{
				RACKVOWrapperExamplesTargetBlock: targetBlock,
				RACKVOWrapperExamplesKeyPath: @keypath([[RACTestObject alloc] init], weakTestObjectValue.strongTestObjectValue),
				RACKVOWrapperExamplesChangeBlock: changeBlock,
				RACKVOWrapperExamplesValueBlock: valueBlock,
				RACKVOWrapperExamplesChangesValueDirectly: @NO
			});
		});

		it(@"should not notice deallocation of the object returned by a dynamic final property", ^{
			RACTestObject *object = [[RACTestObject alloc] init];

			__block id lastValue = nil;
			@autoreleasepool {
				[object rac_observeKeyPath:@keypath(object.dynamicObjectProperty) options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change) {
					lastValue = value;
				}];

				expect(lastValue).to.beKindOf(RACTestObject.class);
			}

			expect(lastValue).to.beKindOf(RACTestObject.class);
		});

		it(@"should not notice deallocation of the object returned by a dynamic intermediate property", ^{
			RACTestObject *object = [[RACTestObject alloc] init];

			__block id lastValue = nil;
			@autoreleasepool {
				[object rac_observeKeyPath:@keypath(object.dynamicObjectProperty.integerValue) options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change) {
					lastValue = value;
				}];

				expect(lastValue).to.equal(@42);
			}

			expect(lastValue).to.equal(@42);
		});

		it(@"should not notice deallocation of the object returned by a dynamic method", ^{
			RACTestObject *object = [[RACTestObject alloc] init];

			__block id lastValue = nil;
			@autoreleasepool {
				[object rac_observeKeyPath:@keypath(object.dynamicObjectMethod) options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change) {
					lastValue = value;
				}];

				expect(lastValue).to.beKindOf(RACTestObject.class);
			}

			expect(lastValue).to.beKindOf(RACTestObject.class);
		});
	});

	it(@"should not call the callback block when the value is the observer", ^{
		__block BOOL observerDisposed = NO;
		__block BOOL observerDeallocationTriggeredChange = NO;
		__block BOOL targetDisposed = NO;
		__block BOOL targetDeallocationTriggeredChange = NO;

		@autoreleasepool {
			RACTestObject *observer __attribute__((objc_precise_lifetime)) = [RACTestObject new];
			[observer.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
				observerDisposed = YES;
			}]];

			RACTestObject *target __attribute__((objc_precise_lifetime)) = [RACTestObject new];
			[target.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
				targetDisposed = YES;
			}]];

			observer.weakTestObjectValue = observer;
			target.weakTestObjectValue = target;

			// These observations can only result in dealloc triggered callbacks.
			[observer rac_observeKeyPath:@keypath(target.weakTestObjectValue) options:0 observer:observer block:^(id _, NSDictionary *__) {
				observerDeallocationTriggeredChange = YES;
			}];

			[target rac_observeKeyPath:@keypath(target.weakTestObjectValue) options:0 observer:observer block:^(id _, NSDictionary *__) {
				targetDeallocationTriggeredChange = YES;
			}];
		}

		expect(observerDisposed).to.beTruthy();
		expect(observerDeallocationTriggeredChange).to.beFalsy();

		expect(targetDisposed).to.beTruthy();
		expect(targetDeallocationTriggeredChange).to.beTruthy();
	});

	it(@"should call the callback block for deallocation of the initial value of a single-key key path", ^{
		RACTestObject *target = [RACTestObject new];
		__block BOOL objectDisposed = NO;
		__block BOOL objectDeallocationTriggeredChange = NO;

		@autoreleasepool {
			RACTestObject *object __attribute__((objc_precise_lifetime)) = [RACTestObject new];
			target.weakTestObjectValue = object;
			[object.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
				objectDisposed = YES;
			}]];

			[target rac_observeKeyPath:@keypath(target.weakTestObjectValue) options:0 observer:target block:^(id _, NSDictionary *__) {
				objectDeallocationTriggeredChange = YES;
			}];
		}

		expect(objectDisposed).to.beTruthy();
		expect(objectDeallocationTriggeredChange).to.beTruthy();
	});
});

describe(@"rac_addObserver:forKeyPath:options:block:", ^{
	it(@"should add and remove an observer", ^{
		NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{}];
		expect(operation).notTo.beNil();

		__block BOOL notified = NO;
		RACDisposable *disposable = [operation rac_observeKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew observer:self block:^(id value, NSDictionary *change) {
			expect([change objectForKey:NSKeyValueChangeNewKey]).to.equal(@YES);

			expect(notified).to.beFalsy();
			notified = YES;
		}];

		expect(disposable).notTo.beNil();

		[operation start];
		[operation waitUntilFinished];

		expect(notified).will.beTruthy();
	});

	it(@"should accept a nil observer", ^{
		NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{}];
		RACDisposable *disposable = [operation rac_observeKeyPath:@"isFinished" options:NSKeyValueObservingOptionNew observer:nil block:^(id value, NSDictionary *change) {}];

		expect(disposable).notTo.beNil();
	});

	it(@"automatically stops KVO on subclasses when the target deallocates", ^{
		void (^testKVOOnSubclass)(Class targetClass, id observer) = ^(Class targetClass, id observer) {
			__weak id weakTarget = nil;
			__weak id identifier = nil;

			@autoreleasepool {
				// Create an observable target that we control the memory management of.
				CFTypeRef target = CFBridgingRetain([[targetClass alloc] init]);
				expect(target).notTo.beNil();

				weakTarget = (__bridge id)target;
				expect(weakTarget).notTo.beNil();

				identifier = [(__bridge id)target rac_observeKeyPath:@"isFinished" options:0 observer:observer block:^(id value, NSDictionary *change) {}];
				expect(identifier).notTo.beNil();

				CFRelease(target);
			}

			expect(weakTarget).to.beNil();
			expect(identifier).to.beNil();
		};

		it (@"stops KVO on NSObject subclasses", ^{
			testKVOOnSubclass(NSOperation.class, self);
		});

		it(@"stops KVO on subclasses of already-swizzled classes", ^{
			testKVOOnSubclass(RACTestOperation.class, self);
		});

		it (@"stops KVO on NSObject subclasses even with a nil observer", ^{
			testKVOOnSubclass(NSOperation.class, nil);
		});

		it(@"stops KVO on subclasses of already-swizzled classes even with a nil observer", ^{
			testKVOOnSubclass(RACTestOperation.class, nil);
		});
	});

	it(@"should automatically stop KVO when the observer deallocates", ^{
		__weak id weakObserver = nil;
		__weak id identifier = nil;

		NSOperation *operation = [[NSOperation alloc] init];

		@autoreleasepool {
			// Create an observer that we control the memory management of.
			CFTypeRef observer = CFBridgingRetain([[NSOperation alloc] init]);
			expect(observer).notTo.beNil();

			weakObserver = (__bridge id)observer;
			expect(weakObserver).notTo.beNil();

			identifier = [operation rac_observeKeyPath:@"isFinished" options:0 observer:(__bridge id)observer block:^(id value, NSDictionary *change) {}];
			expect(identifier).notTo.beNil();

			CFRelease(observer);
		}

		expect(weakObserver).to.beNil();
	});

	it(@"should stop KVO when the observer is disposed", ^{
		NSOperationQueue *queue = [[NSOperationQueue alloc] init];
		__block NSString *name = nil;

		RACDisposable *disposable = [queue rac_observeKeyPath:@"name" options:0 observer:self block:^(id value, NSDictionary *change) {
			name = queue.name;
		}];

		queue.name = @"1";
		expect(name).to.equal(@"1");
		[disposable dispose];
		queue.name = @"2";
		expect(name).to.equal(@"1");
	});

	it(@"should distinguish between observers being disposed", ^{
		NSOperationQueue *queue = [[NSOperationQueue alloc] init];
		__block NSString *name1 = nil;
		__block NSString *name2 = nil;

		RACDisposable *disposable = [queue rac_observeKeyPath:@"name" options:0 observer:self block:^(id value, NSDictionary *change) {
			name1 = queue.name;
		}];
		[queue rac_observeKeyPath:@"name" options:0 observer:self block:^(id value, NSDictionary *change) {
			name2 = queue.name;
		}];

		queue.name = @"1";
		expect(name1).to.equal(@"1");
		expect(name2).to.equal(@"1");
		[disposable dispose];
		queue.name = @"2";
		expect(name1).to.equal(@"1");
		expect(name2).to.equal(@"2");
	});
});

SpecEnd

@implementation RACTestOperation
@end
