//
//  RACSequenceSpec.m
//  ReactiveCocoa
//
//  Created by Justin Spahr-Summers on 2012-11-01.
//  Copyright (c) 2012 GitHub, Inc. All rights reserved.
//

#import "RACSequenceExamples.h"
#import "RACStreamExamples.h"

#import "NSArray+RACSequenceAdditions.h"
#import "NSObject+RACDeallocating.h"
#import "NSObject+RACPropertySubscribing.h"
#import "RACCompoundDisposable.h"
#import "RACDisposable.h"
#import "RACSequence.h"
#import "RACUnit.h"

SpecBegin(RACSequence)

describe(@"RACStream", ^{
	id verifyValues = ^(RACSequence *sequence, NSArray *expectedValues) {
		NSMutableArray *collectedValues = [NSMutableArray array];
		while (sequence.head != nil) {
			[collectedValues addObject:sequence.head];
			sequence = sequence.tail;
		}

		expect(collectedValues).to.equal(expectedValues);
	};

	__block RACSequence *infiniteSequence = [RACSequence sequenceWithHeadBlock:^{
		return RACUnit.defaultUnit;
	} tailBlock:^{
		return infiniteSequence;
	}];

	itShouldBehaveLike(RACStreamExamples, ^{
		return @{
			RACStreamExamplesClass: RACSequence.class,
			RACStreamExamplesVerifyValuesBlock: verifyValues,
			RACStreamExamplesInfiniteStream: infiniteSequence
		};
	});
});

describe(@"+sequenceWithHeadBlock:tailBlock:", ^{
	__block RACSequence *sequence;
	__block BOOL headInvoked;
	__block BOOL tailInvoked;

	before(^{
		headInvoked = NO;
		tailInvoked = NO;

		sequence = [RACSequence sequenceWithHeadBlock:^{
			headInvoked = YES;
			return @0;
		} tailBlock:^{
			tailInvoked = YES;
			return [RACSequence return:@1];
		}];

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

	it(@"should use the values from the head and tail blocks", ^{
		expect(sequence.head).to.equal(@0);
		expect(sequence.tail.head).to.equal(@1);
		expect(sequence.tail.tail).to.beNil();
	});

	it(@"should lazily invoke head and tail blocks", ^{
		expect(headInvoked).to.beFalsy();
		expect(tailInvoked).to.beFalsy();

		expect(sequence.head).to.equal(@0);
		expect(headInvoked).to.beTruthy();
		expect(tailInvoked).to.beFalsy();

		expect(sequence.tail).notTo.beNil();
		expect(tailInvoked).to.beTruthy();
	});

	after(^{
		itShouldBehaveLike(RACSequenceExamples, ^{
			return @{
				RACSequenceExampleSequence: sequence,
				RACSequenceExampleExpectedValues: @[ @0, @1 ]
			};
		});
	});
});

describe(@"empty sequences", ^{
	itShouldBehaveLike(RACSequenceExamples, ^{
		return @{
			RACSequenceExampleSequence: [RACSequence empty],
			RACSequenceExampleExpectedValues: @[]
		};
	});
});

describe(@"non-empty sequences", ^{
	itShouldBehaveLike(RACSequenceExamples, ^{
		return @{
			RACSequenceExampleSequence: [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]],
			RACSequenceExampleExpectedValues: @[ @0, @1, @2 ]
		};
	});
});

describe(@"eager sequences", ^{
	__block RACSequence *lazySequence;
	__block BOOL headInvoked;
	__block BOOL tailInvoked;

	NSArray *values = @[ @0, @1 ];
	
	before(^{
		headInvoked = NO;
		tailInvoked = NO;
		
		lazySequence = [RACSequence sequenceWithHeadBlock:^{
			headInvoked = YES;
			return @0;
		} tailBlock:^{
			tailInvoked = YES;
			return [RACSequence return:@1];
		}];
		
		expect(lazySequence).notTo.beNil();
	});
	
	itShouldBehaveLike(RACSequenceExamples, ^{
		return @{
			RACSequenceExampleSequence: lazySequence.eagerSequence,
			RACSequenceExampleExpectedValues: values
		};
	});
	
	it(@"should evaluate all values immediately", ^{
		RACSequence *eagerSequence = lazySequence.eagerSequence;
		expect(headInvoked).to.beTruthy();
		expect(tailInvoked).to.beTruthy();
		expect(eagerSequence.array).to.equal(values);
	});
});

describe(@"-take:", ^{
	it(@"should complete take: without needing the head of the second item in the sequence", ^{
		__block NSUInteger valuesTaken = 0;

		__block RACSequence *sequence = [RACSequence sequenceWithHeadBlock:^{
			++valuesTaken;
			return RACUnit.defaultUnit;
		} tailBlock:^{
			return sequence;
		}];

		NSArray *values = [sequence take:1].array;
		expect(values).to.equal(@[ RACUnit.defaultUnit ]);
		expect(valuesTaken).to.equal(1);
	});
});

describe(@"-bind:", ^{
	it(@"should only evaluate head when the resulting sequence is evaluated", ^{
		__block BOOL headInvoked = NO;

		RACSequence *original = [RACSequence sequenceWithHeadBlock:^{
			headInvoked = YES;
			return RACUnit.defaultUnit;
		} tailBlock:^ id {
			return nil;
		}];

		RACSequence *bound = [original bind:^{
			return ^(id value, BOOL *stop) {
				return [RACSequence return:value];
			};
		}];

		expect(bound).notTo.beNil();
		expect(headInvoked).to.beFalsy();

		expect(bound.head).to.equal(RACUnit.defaultUnit);
		expect(headInvoked).to.beTruthy();
	});
});

describe(@"-objectEnumerator", ^{
	it(@"should only evaluate head as it's enumerated", ^{
		__block BOOL firstHeadInvoked = NO;
		__block BOOL secondHeadInvoked = NO;
		__block BOOL thirdHeadInvoked = NO;
		
		RACSequence *sequence = [RACSequence sequenceWithHeadBlock:^id{
			firstHeadInvoked = YES;
			return @1;
		} tailBlock:^RACSequence *{
			return [RACSequence sequenceWithHeadBlock:^id{
				secondHeadInvoked = YES;
				return @2;
			} tailBlock:^RACSequence *{
				return [RACSequence sequenceWithHeadBlock:^id{
					thirdHeadInvoked = YES;
					return @3;
				} tailBlock:^RACSequence *{
					return RACSequence.empty;
				}];
			}];
		}];
		NSEnumerator *enumerator = sequence.objectEnumerator;
		
		expect(firstHeadInvoked).to.beFalsy();
		expect(secondHeadInvoked).to.beFalsy();
		expect(thirdHeadInvoked).to.beFalsy();
		
		expect([enumerator nextObject]).to.equal(@1);
		
		expect(firstHeadInvoked).to.beTruthy();
		expect(secondHeadInvoked).to.beFalsy();
		expect(thirdHeadInvoked).to.beFalsy();
		
		expect([enumerator nextObject]).to.equal(@2);
		
		expect(secondHeadInvoked).to.beTruthy();
		expect(thirdHeadInvoked).to.beFalsy();
		
		expect([enumerator nextObject]).to.equal(@3);
		
		expect(thirdHeadInvoked).to.beTruthy();
		
		expect([enumerator nextObject]).to.beNil();
	});
	
	it(@"should let the sequence dealloc as it's enumerated", ^{
		__block BOOL firstSequenceDeallocd = NO;
		__block BOOL secondSequenceDeallocd = NO;
		__block BOOL thirdSequenceDeallocd = NO;
		
		NSEnumerator *enumerator = nil;
		
		@autoreleasepool {
			RACSequence *thirdSequence __attribute__((objc_precise_lifetime)) = [RACSequence sequenceWithHeadBlock:^id{
				return @3;
			} tailBlock:^RACSequence *{
				return RACSequence.empty;
			}];
			[thirdSequence.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
				thirdSequenceDeallocd = YES;
			}]];
			
			RACSequence *secondSequence __attribute__((objc_precise_lifetime)) = [RACSequence sequenceWithHeadBlock:^id{
				return @2;
			} tailBlock:^RACSequence *{
				return thirdSequence;
			}];
			[secondSequence.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
				secondSequenceDeallocd = YES;
			}]];
			
			RACSequence *firstSequence __attribute__((objc_precise_lifetime)) = [RACSequence sequenceWithHeadBlock:^id{
				return @1;
			} tailBlock:^RACSequence *{
				return secondSequence;
			}];
			[firstSequence.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{
				firstSequenceDeallocd = YES;
			}]];
			
			enumerator = firstSequence.objectEnumerator;
		}
		
		@autoreleasepool {
			expect([enumerator nextObject]).to.equal(@1);
		}

		@autoreleasepool {
			expect([enumerator nextObject]).to.equal(@2);
		}
		expect(firstSequenceDeallocd).will.beTruthy();
		
		@autoreleasepool {
			expect([enumerator nextObject]).to.equal(@3);
		}
		expect(secondSequenceDeallocd).will.beTruthy();
		
		@autoreleasepool {
			expect([enumerator nextObject]).to.beNil();
		}
		expect(thirdSequenceDeallocd).will.beTruthy();
	});
});

it(@"shouldn't overflow the stack when deallocated on a background queue", ^{
	NSUInteger length = 10000;
	NSMutableArray *values = [NSMutableArray arrayWithCapacity:length];
	for (NSUInteger i = 0; i < length; ++i) {
		[values addObject:@(i)];
	}

	__block BOOL finished = NO;
	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
		@autoreleasepool {
			[[values.rac_sequence map:^(id value) {
				return value;
			}] array];
		}

		finished = YES;
	});

	NSTimeInterval oldTimeout = Expecta.asynchronousTestTimeout;
	Expecta.asynchronousTestTimeout = DBL_MAX;
	expect(finished).will.beTruthy();
	Expecta.asynchronousTestTimeout = oldTimeout;
});

describe(@"-foldLeftWithStart:reduce:", ^{
	it(@"should reduce with start first", ^{
		RACSequence *sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
		NSNumber *result = [sequence foldLeftWithStart:@3 reduce:^(NSNumber *first, NSNumber *rest) {
			return first;
		}];
		expect(result).to.equal(@3);
	});

	it(@"should be left associative", ^{
		RACSequence *sequence = [[[RACSequence return:@1] concat:[RACSequence return:@2]] concat:[RACSequence return:@3]];
		NSNumber *result = [sequence foldLeftWithStart:@0 reduce:^(NSNumber *first, NSNumber *rest) {
			int difference = first.intValue - rest.intValue;
			return @(difference);
		}];
		expect(result).to.equal(@-6);
	});
});

describe(@"-foldRightWithStart:reduce:", ^{
	it(@"should be lazy", ^{
		__block BOOL headInvoked = NO;
		__block BOOL tailInvoked = NO;
		RACSequence *sequence = [RACSequence sequenceWithHeadBlock:^{
			headInvoked = YES;
			return @0;
		} tailBlock:^{
			tailInvoked = YES;
			return [RACSequence return:@1];
		}];
		
		NSNumber *result = [sequence foldRightWithStart:@2 reduce:^(NSNumber *first, RACSequence *rest) {
			return first;
		}];
		
		expect(result).to.equal(@0);
		expect(headInvoked).to.beTruthy();
		expect(tailInvoked).to.beFalsy();
	});
	
	it(@"should reduce with start last", ^{
		RACSequence *sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
		NSNumber *result = [sequence foldRightWithStart:@3 reduce:^(NSNumber *first, RACSequence *rest) {
			return rest.head;
		}];
		expect(result).to.equal(@3);
	});
	
	it(@"should be right associative", ^{
		RACSequence *sequence = [[[RACSequence return:@1] concat:[RACSequence return:@2]] concat:[RACSequence return:@3]];
		NSNumber *result = [sequence foldRightWithStart:@0 reduce:^(NSNumber *first, RACSequence *rest) {
			int difference = first.intValue - [rest.head intValue];
			return @(difference);
		}];
		expect(result).to.equal(@2);
	});
});

describe(@"-any", ^{
	__block RACSequence *sequence;
	beforeEach(^{
		sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
	});
	
	it(@"should return true when at least one exists", ^{
		BOOL result = [sequence any:^ BOOL (NSNumber *value) {
			return value.integerValue > 0;
		}];
		expect(result).to.beTruthy();
	});
	
	it(@"should return false when no such thing exists", ^{
		BOOL result = [sequence any:^ BOOL (NSNumber *value) {
			return value.integerValue == 3;
		}];
		expect(result).to.beFalsy();
	});
});

describe(@"-all", ^{
	__block RACSequence *sequence;
	beforeEach(^{
		sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
	});
	
	it(@"should return true when all values pass", ^{
		BOOL result = [sequence all:^ BOOL (NSNumber *value) {
			return value.integerValue >= 0;
		}];
		expect(result).to.beTruthy();
	});
	
	it(@"should return false when at least one value fails", ^{
		BOOL result = [sequence all:^ BOOL (NSNumber *value) {
			return value.integerValue < 2;
		}];
		expect(result).to.beFalsy();
	});
});

describe(@"-objectPassingTest:", ^{
	__block RACSequence *sequence;
	beforeEach(^{
		sequence = [[[RACSequence return:@0] concat:[RACSequence return:@1]] concat:[RACSequence return:@2]];
	});
	
	it(@"should return leftmost object that passes the test", ^{
		NSNumber *result = [sequence objectPassingTest:^ BOOL (NSNumber *value) {
			return value.intValue > 0;
		}];
		expect(result).to.equal(@1);
	});
	
	it(@"should return nil if no objects pass the test", ^{
		NSNumber *result = [sequence objectPassingTest:^ BOOL (NSNumber *value) {
			return value.intValue < 0;
		}];
		expect(result).to.beNil();
	});
});

SpecEnd
