//
//  RACSchedulerSpec.m
//  ReactiveCocoa
//
//  Created by Josh Abernathy on 11/29/12.
//  Copyright (c) 2012 GitHub, Inc. All rights reserved.
//

#import "RACScheduler.h"
#import "RACScheduler+Private.h"
#import "RACQueueScheduler+Subclass.h"
#import "RACDisposable.h"
#import "EXTScope.h"
#import "RACTestExampleScheduler.h"
#import <libkern/OSAtomic.h>

// This shouldn't be used directly. Use the `expectCurrentSchedulers` block
// below instead.
static void expectCurrentSchedulersInner(NSArray *schedulers, NSMutableArray *currentSchedulerArray) {
	if (schedulers.count > 0) {
		RACScheduler *topScheduler = schedulers[0];
		[topScheduler schedule:^{
			RACScheduler *currentScheduler = RACScheduler.currentScheduler;
			if (currentScheduler != nil) [currentSchedulerArray addObject:currentScheduler];
			expectCurrentSchedulersInner([schedulers subarrayWithRange:NSMakeRange(1, schedulers.count - 1)], currentSchedulerArray);
		}];
	}
}

SpecBegin(RACScheduler)

it(@"should know its current scheduler", ^{
	// Recursively schedules a block in each of the given schedulers and records
	// the +currentScheduler at each step. It then expects the array of
	// +currentSchedulers and the expected array to be equal.
	//
	// schedulers                - The array of schedulers to recursively schedule.
	// expectedCurrentSchedulers - The array of +currentSchedulers to expect.
	void (^expectCurrentSchedulers)(NSArray *, NSArray *) = ^(NSArray *schedulers, NSArray *expectedCurrentSchedulers) {
		NSMutableArray *currentSchedulerArray = [NSMutableArray array];
		expectCurrentSchedulersInner(schedulers, currentSchedulerArray);
		expect(currentSchedulerArray).will.equal(expectedCurrentSchedulers);
	};

	RACScheduler *backgroundScheduler = [RACScheduler scheduler];

	expectCurrentSchedulers(@[ backgroundScheduler, RACScheduler.immediateScheduler ], @[ backgroundScheduler, backgroundScheduler ]);
	expectCurrentSchedulers(@[ backgroundScheduler, RACScheduler.subscriptionScheduler ], @[ backgroundScheduler, backgroundScheduler ]);

	NSArray *mainThreadJumper = @[ RACScheduler.mainThreadScheduler, backgroundScheduler, RACScheduler.mainThreadScheduler ];
	expectCurrentSchedulers(mainThreadJumper, mainThreadJumper);

	NSArray *backgroundJumper = @[ backgroundScheduler, RACScheduler.mainThreadScheduler, backgroundScheduler ];
	expectCurrentSchedulers(backgroundJumper, backgroundJumper);
});

describe(@"+mainThreadScheduler", ^{
	it(@"should cancel scheduled blocks when disposed", ^{
		__block BOOL firstBlockRan = NO;
		__block BOOL secondBlockRan = NO;

		RACDisposable *disposable = [RACScheduler.mainThreadScheduler schedule:^{
			firstBlockRan = YES;
		}];

		expect(disposable).notTo.beNil();

		[RACScheduler.mainThreadScheduler schedule:^{
			secondBlockRan = YES;
		}];

		[disposable dispose];

		expect(secondBlockRan).to.beFalsy();
		expect(secondBlockRan).will.beTruthy();
		expect(firstBlockRan).to.beFalsy();
	});

	it(@"should schedule future blocks", ^{
		__block BOOL done = NO;

		[RACScheduler.mainThreadScheduler after:[NSDate date] schedule:^{
			done = YES;
		}];

		expect(done).to.beFalsy();
		expect(done).will.beTruthy();
	});

	it(@"should cancel future blocks when disposed", ^{
		__block BOOL firstBlockRan = NO;
		__block BOOL secondBlockRan = NO;

		RACDisposable *disposable = [RACScheduler.mainThreadScheduler after:[NSDate date] schedule:^{
			firstBlockRan = YES;
		}];

		expect(disposable).notTo.beNil();

		[RACScheduler.mainThreadScheduler after:[NSDate date] schedule:^{
			secondBlockRan = YES;
		}];

		[disposable dispose];

		expect(secondBlockRan).to.beFalsy();
		expect(secondBlockRan).will.beTruthy();
		expect(firstBlockRan).to.beFalsy();
	});

	it(@"should schedule recurring blocks", ^{
		__block NSUInteger count = 0;

		RACDisposable *disposable = [RACScheduler.mainThreadScheduler after:[NSDate date] repeatingEvery:0.05 withLeeway:0 schedule:^{
			count++;
		}];

		expect(count).to.equal(0);
		expect(count).will.equal(1);
		expect(count).will.equal(2);
		expect(count).will.equal(3);

		[disposable dispose];
		[NSRunLoop.mainRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];

		expect(count).to.equal(3);
	});
});

describe(@"+scheduler", ^{
	__block RACScheduler *scheduler;
	__block NSDate * (^futureDate)(void);

	beforeEach(^{
		scheduler = [RACScheduler scheduler];

		futureDate = ^{
			return [NSDate dateWithTimeIntervalSinceNow:0.01];
		};
	});

	it(@"should cancel scheduled blocks when disposed", ^{
		__block BOOL firstBlockRan = NO;
		__block BOOL secondBlockRan = NO;

		// Start off on the scheduler so the enqueued blocks won't run until we
		// return.
		[scheduler schedule:^{
			RACDisposable *disposable = [scheduler schedule:^{
				firstBlockRan = YES;
			}];

			expect(disposable).notTo.beNil();

			[scheduler schedule:^{
				secondBlockRan = YES;
			}];

			[disposable dispose];
		}];

		expect(secondBlockRan).will.beTruthy();
		expect(firstBlockRan).to.beFalsy();
	});

	it(@"should schedule future blocks", ^{
		__block BOOL done = NO;

		[scheduler after:futureDate() schedule:^{
			done = YES;
		}];

		expect(done).to.beFalsy();
		expect(done).will.beTruthy();
	});

	it(@"should cancel future blocks when disposed", ^{
		__block BOOL firstBlockRan = NO;
		__block BOOL secondBlockRan = NO;

		NSDate *date = futureDate();
		RACDisposable *disposable = [scheduler after:date schedule:^{
			firstBlockRan = YES;
		}];

		expect(disposable).notTo.beNil();
		[disposable dispose];

		[scheduler after:date schedule:^{
			secondBlockRan = YES;
		}];

		expect(secondBlockRan).to.beFalsy();
		expect(secondBlockRan).will.beTruthy();
		expect(firstBlockRan).to.beFalsy();
	});

	it(@"should schedule recurring blocks", ^{
		__block NSUInteger count = 0;

		RACDisposable *disposable = [scheduler after:[NSDate date] repeatingEvery:0.05 withLeeway:0 schedule:^{
			count++;
		}];

		expect(count).to.equal(0);
		expect(count).will.equal(1);
		expect(count).will.equal(2);
		expect(count).will.equal(3);

		[disposable dispose];
		[NSThread sleepForTimeInterval:0.1];

		expect(count).to.equal(3);
	});
});

describe(@"+subscriptionScheduler", ^{
	describe(@"setting +currentScheduler", ^{
		__block RACScheduler *currentScheduler;

		beforeEach(^{
			currentScheduler = nil;
		});

		it(@"should be the +mainThreadScheduler when scheduled from the main queue", ^{
			dispatch_async(dispatch_get_main_queue(), ^{
				[RACScheduler.subscriptionScheduler schedule:^{
					currentScheduler = RACScheduler.currentScheduler;
				}];
			});

			expect(currentScheduler).will.equal(RACScheduler.mainThreadScheduler);
		});

		it(@"should be a +scheduler when scheduled from an unknown queue", ^{
			dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
				[RACScheduler.subscriptionScheduler schedule:^{
					currentScheduler = RACScheduler.currentScheduler;
				}];
			});

			expect(currentScheduler).willNot.beNil();
			expect(currentScheduler).notTo.equal(RACScheduler.mainThreadScheduler);
		});

		it(@"should equal the background scheduler from which the block was scheduled", ^{
			RACScheduler *backgroundScheduler = [RACScheduler scheduler];
			[backgroundScheduler schedule:^{
				[RACScheduler.subscriptionScheduler schedule:^{
					currentScheduler = RACScheduler.currentScheduler;
				}];
			}];

			expect(currentScheduler).will.equal(backgroundScheduler);
		});
	});

	it(@"should execute scheduled blocks immediately if it's in a scheduler already", ^{
		__block BOOL done = NO;
		__block BOOL executedImmediately = NO;

		[[RACScheduler scheduler] schedule:^{
			[RACScheduler.subscriptionScheduler schedule:^{
				executedImmediately = YES;
			}];

			done = YES;
		}];

		expect(done).will.beTruthy();
		expect(executedImmediately).to.beTruthy();
	});
});

describe(@"+immediateScheduler", ^{
	it(@"should immediately execute scheduled blocks", ^{
		__block BOOL executed = NO;
		RACDisposable *disposable = [RACScheduler.immediateScheduler schedule:^{
			executed = YES;
		}];

		expect(disposable).to.beNil();
		expect(executed).to.beTruthy();
	});

	it(@"should block for future scheduled blocks", ^{
		__block BOOL executed = NO;
		RACDisposable *disposable = [RACScheduler.immediateScheduler after:[NSDate dateWithTimeIntervalSinceNow:0.01] schedule:^{
			executed = YES;
		}];

		expect(executed).to.beTruthy();
		expect(disposable).to.beNil();
	});
});

describe(@"-scheduleRecursiveBlock:", ^{
	describe(@"with a synchronous scheduler", ^{
		it(@"should behave like a normal block when it doesn't invoke itself", ^{
			__block BOOL executed = NO;
			[RACScheduler.immediateScheduler scheduleRecursiveBlock:^(void (^recurse)(void)) {
				expect(executed).to.beFalsy();
				executed = YES;
			}];

			expect(executed).to.beTruthy();
		});

		it(@"should reschedule itself after the caller completes", ^{
			__block NSUInteger count = 0;
			[RACScheduler.immediateScheduler scheduleRecursiveBlock:^(void (^recurse)(void)) {
				NSUInteger thisCount = ++count;
				if (thisCount < 3) {
					recurse();

					// The block shouldn't have been invoked again yet, only
					// scheduled.
					expect(count).to.equal(thisCount);
				}
			}];

			expect(count).to.equal(3);
		});
	});

	describe(@"with an asynchronous scheduler", ^{
		it(@"should behave like a normal block when it doesn't invoke itself", ^{
			__block BOOL executed = NO;
			[RACScheduler.mainThreadScheduler scheduleRecursiveBlock:^(void (^recurse)(void)) {
				expect(executed).to.beFalsy();
				executed = YES;
			}];

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

		it(@"should reschedule itself after the caller completes", ^{
			__block NSUInteger count = 0;
			[RACScheduler.mainThreadScheduler scheduleRecursiveBlock:^(void (^recurse)(void)) {
				NSUInteger thisCount = ++count;
				if (thisCount < 3) {
					recurse();

					// The block shouldn't have been invoked again yet, only
					// scheduled.
					expect(count).to.equal(thisCount);
				}
			}];

			expect(count).will.equal(3);
		});

		it(@"should reschedule when invoked asynchronously", ^{
			__block NSUInteger count = 0;

			RACScheduler *asynchronousScheduler = [RACScheduler scheduler];
			[RACScheduler.immediateScheduler scheduleRecursiveBlock:^(void (^recurse)(void)) {
				[asynchronousScheduler after:[NSDate dateWithTimeIntervalSinceNow:0.01] schedule:^{
					NSUInteger thisCount = ++count;
					if (thisCount < 3) {
						recurse();

						// The block shouldn't have been invoked again yet, only
						// scheduled.
						expect(count).to.equal(thisCount);
					}
				}];
			}];

			expect(count).will.equal(3);
		});

		it(@"shouldn't reschedule itself when disposed", ^{
			__block NSUInteger count = 0;
			__block RACDisposable *disposable = [RACScheduler.mainThreadScheduler scheduleRecursiveBlock:^(void (^recurse)(void)) {
				++count;

				expect(disposable).notTo.beNil();
				[disposable dispose];

				recurse();
			}];

			expect(count).will.equal(1);
		});
	});
});

describe(@"subclassing", ^{
	__block RACTestExampleScheduler *scheduler;

	beforeEach(^{
		scheduler = [[RACTestExampleScheduler alloc] initWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)];
	});

	it(@"should invoke blocks scheduled with -schedule:", ^{
		__block BOOL invoked = NO;
		[scheduler schedule:^{
			invoked = YES;
		}];

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

	it(@"should invoke blocks scheduled with -after:schedule:", ^{
		__block BOOL invoked = NO;
		[scheduler after:[NSDate dateWithTimeIntervalSinceNow:0.01] schedule:^{
			invoked = YES;
		}];
		
		expect(invoked).will.beTruthy();
	});

	it(@"should set a valid current scheduler", ^{
		__block RACScheduler *currentScheduler;
		[scheduler schedule:^{
			currentScheduler = RACScheduler.currentScheduler;
		}];

		expect(currentScheduler).will.equal(scheduler);
	});
});

SpecEnd
