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

#import "RACStreamExamples.h"

#import "RACStream.h"
#import "RACUnit.h"
#import "RACTuple.h"

NSString * const RACStreamExamples = @"RACStreamExamples";
NSString * const RACStreamExamplesClass = @"RACStreamExamplesClass";
NSString * const RACStreamExamplesInfiniteStream = @"RACStreamExamplesInfiniteStream";
NSString * const RACStreamExamplesVerifyValuesBlock = @"RACStreamExamplesVerifyValuesBlock";

SharedExampleGroupsBegin(RACStreamExamples)

sharedExamplesFor(RACStreamExamples, ^(NSDictionary *data) {
	__block Class streamClass;
	__block void (^verifyValues)(RACStream *, NSArray *);
	__block RACStream *infiniteStream;

	__block RACStream *(^streamWithValues)(NSArray *);
	
	before(^{
		streamClass = data[RACStreamExamplesClass];
		verifyValues = data[RACStreamExamplesVerifyValuesBlock];
		infiniteStream = data[RACStreamExamplesInfiniteStream];
		streamWithValues = [^(NSArray *values) {
			RACStream *stream = [streamClass empty];

			for (id value in values) {
				stream = [stream concat:[streamClass return:value]];
			}

			return stream;
		} copy];
	});

	it(@"should return an empty stream", ^{
		RACStream *stream = [streamClass empty];
		verifyValues(stream, @[]);
	});

	it(@"should lift a value into a stream", ^{
		RACStream *stream = [streamClass return:RACUnit.defaultUnit];
		verifyValues(stream, @[ RACUnit.defaultUnit ]);
	});

	describe(@"-concat:", ^{
		it(@"should concatenate two streams", ^{
			RACStream *stream = [[streamClass return:@0] concat:[streamClass return:@1]];
			verifyValues(stream, @[ @0, @1 ]);
		});

		it(@"should concatenate three streams", ^{
			RACStream *stream = [[[streamClass return:@0] concat:[streamClass return:@1]] concat:[streamClass return:@2]];
			verifyValues(stream, @[ @0, @1, @2 ]);
		});

		it(@"should concatenate around an empty stream", ^{
			RACStream *stream = [[[streamClass return:@0] concat:[streamClass empty]] concat:[streamClass return:@2]];
			verifyValues(stream, @[ @0, @2 ]);
		});
	});

	it(@"should flatten", ^{
		RACStream *stream = [[streamClass return:[streamClass return:RACUnit.defaultUnit]] flatten];
		verifyValues(stream, @[ RACUnit.defaultUnit ]);
	});

	describe(@"-bind:", ^{
		it(@"should return the result of binding a single value", ^{
			RACStream *stream = [[streamClass return:@0] bind:^{
				return ^(NSNumber *value, BOOL *stop) {
					NSNumber *newValue = @(value.integerValue + 1);
					return [streamClass return:newValue];
				};
			}];

			verifyValues(stream, @[ @1 ]);
		});

		it(@"should concatenate the result of binding multiple values", ^{
			RACStream *baseStream = streamWithValues(@[ @0, @1 ]);
			RACStream *stream = [baseStream bind:^{
				return ^(NSNumber *value, BOOL *stop) {
					NSNumber *newValue = @(value.integerValue + 1);
					return [streamClass return:newValue];
				};
			}];

			verifyValues(stream, @[ @1, @2 ]);
		});

		it(@"should concatenate with an empty result from binding a value", ^{
			RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
			RACStream *stream = [baseStream bind:^{
				return ^(NSNumber *value, BOOL *stop) {
					if (value.integerValue == 1) return [streamClass empty];

					NSNumber *newValue = @(value.integerValue + 1);
					return [streamClass return:newValue];
				};
			}];

			verifyValues(stream, @[ @1, @3 ]);
		});

		it(@"should terminate immediately when returning nil", ^{
			RACStream *stream = [infiniteStream bind:^{
				return ^ id (id _, BOOL *stop) {
					return nil;
				};
			}];

			verifyValues(stream, @[]);
		});

		it(@"should terminate after one value when setting 'stop'", ^{
			RACStream *stream = [infiniteStream bind:^{
				return ^ id (id value, BOOL *stop) {
					*stop = YES;
					return [streamClass return:value];
				};
			}];

			verifyValues(stream, @[ RACUnit.defaultUnit ]);
		});

		it(@"should terminate immediately when returning nil and setting 'stop'", ^{
			RACStream *stream = [infiniteStream bind:^{
				return ^ id (id _, BOOL *stop) {
					*stop = YES;
					return nil;
				};
			}];

			verifyValues(stream, @[]);
		});

		it(@"should be restartable even with block state", ^{
			NSArray *values = @[ @0, @1, @2 ];
			RACStream *baseStream = streamWithValues(values);

			RACStream *countingStream = [baseStream bind:^{
				__block NSUInteger counter = 0;

				return ^(id x, BOOL *stop) {
					return [streamClass return:@(counter++)];
				};
			}];

			verifyValues(countingStream, @[ @0, @1, @2 ]);
			verifyValues(countingStream, @[ @0, @1, @2 ]);
		});

		it(@"should be interleavable even with block state", ^{
			NSArray *values = @[ @0, @1, @2 ];
			RACStream *baseStream = streamWithValues(values);

			RACStream *countingStream = [baseStream bind:^{
				__block NSUInteger counter = 0;

				return ^(id x, BOOL *stop) {
					return [streamClass return:@(counter++)];
				};
			}];

			// Just so +zip:reduce: thinks this is a unique stream.
			RACStream *anotherStream = [[streamClass empty] concat:countingStream];

			RACStream *zipped = [streamClass zip:@[ countingStream, anotherStream ] reduce:^(NSNumber *v1, NSNumber *v2) {
				return @(v1.integerValue + v2.integerValue);
			}];

			verifyValues(zipped, @[ @0, @2, @4 ]);
		});
	});

	describe(@"-flattenMap:", ^{
		it(@"should return a single mapped result", ^{
			RACStream *stream = [[streamClass return:@0] flattenMap:^(NSNumber *value) {
				NSNumber *newValue = @(value.integerValue + 1);
				return [streamClass return:newValue];
			}];

			verifyValues(stream, @[ @1 ]);
		});

		it(@"should concatenate the results of mapping multiple values", ^{
			RACStream *baseStream = streamWithValues(@[ @0, @1 ]);
			RACStream *stream = [baseStream flattenMap:^(NSNumber *value) {
				NSNumber *newValue = @(value.integerValue + 1);
				return [streamClass return:newValue];
			}];

			verifyValues(stream, @[ @1, @2 ]);
		});

		it(@"should concatenate with an empty result from mapping a value", ^{
			RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
			RACStream *stream = [baseStream flattenMap:^(NSNumber *value) {
				if (value.integerValue == 1) return [streamClass empty];

				NSNumber *newValue = @(value.integerValue + 1);
				return [streamClass return:newValue];
			}];

			verifyValues(stream, @[ @1, @3 ]);
		});

		it(@"should treat nil streams like empty streams", ^{
			RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
			RACStream *stream = [baseStream flattenMap:^ RACStream * (NSNumber *value) {
				if (value.integerValue == 1) return nil;

				NSNumber *newValue = @(value.integerValue + 1);
				return [streamClass return:newValue];
			}];

			verifyValues(stream, @[ @1, @3 ]);
		});
	});

	it(@"should map", ^{
		RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
		RACStream *stream = [baseStream map:^(NSNumber *value) {
			return @(value.integerValue + 1);
		}];

		verifyValues(stream, @[ @1, @2, @3 ]);
	});

	it(@"should map and replace", ^{
		RACStream *baseStream = streamWithValues(@[ @0, @1, @2 ]);
		RACStream *stream = [baseStream mapReplace:RACUnit.defaultUnit];

		verifyValues(stream, @[ RACUnit.defaultUnit, RACUnit.defaultUnit, RACUnit.defaultUnit ]);
	});

	it(@"should filter", ^{
		RACStream *baseStream = streamWithValues(@[ @0, @1, @2, @3, @4, @5, @6 ]);
		RACStream *stream = [baseStream filter:^ BOOL (NSNumber *value) {
			return value.integerValue % 2 == 0;
		}];

		verifyValues(stream, @[ @0, @2, @4, @6 ]);
	});

	describe(@"-ignore:", ^{
		it(@"should ignore a value", ^{
			RACStream *baseStream = streamWithValues(@[ @0, @1, @2, @3, @4, @5, @6 ]);
			RACStream *stream = [baseStream ignore:@1];

			verifyValues(stream, @[ @0, @2, @3, @4, @5, @6 ]);
		});

		it(@"should ignore based on object equality", ^{
			RACStream *baseStream = streamWithValues(@[ @"0", @"1", @"2", @"3", @"4", @"5", @"6" ]);

			NSMutableString *valueToIgnore = [[NSMutableString alloc] init];
			[valueToIgnore appendString:@"1"];
			RACStream *stream = [baseStream ignore:valueToIgnore];

			verifyValues(stream, @[ @"0", @"2", @"3", @"4", @"5", @"6" ]);
		});
	});

	it(@"should start with a value", ^{
		RACStream *stream = [[streamClass return:@1] startWith:@0];
		verifyValues(stream, @[ @0, @1 ]);
	});

	describe(@"-skip:", ^{
		__block NSArray *values;
		__block RACStream *stream;

		before(^{
			values = @[ @0, @1, @2 ];
			stream = streamWithValues(values);
		});

		it(@"should skip any valid number of values", ^{
			for (NSUInteger i = 0; i < values.count; i++) {
				verifyValues([stream skip:i], [values subarrayWithRange:NSMakeRange(i, values.count - i)]);
			}
		});

		it(@"should return an empty stream when skipping too many values", ^{
			verifyValues([stream skip:4], @[]);
		});
	});

	describe(@"-take:", ^{
		describe(@"with three values", ^{
			__block NSArray *values;
			__block RACStream *stream;

			before(^{
				values = @[ @0, @1, @2 ];
				stream = streamWithValues(values);
			});

			it(@"should take any valid number of values", ^{
				for (NSUInteger i = 0; i < values.count; i++) {
					verifyValues([stream take:i], [values subarrayWithRange:NSMakeRange(0, i)]);
				}
			});

			it(@"should return the same stream when taking too many values", ^{
				verifyValues([stream take:4], values);
			});
		});

		it(@"should take and terminate from an infinite stream", ^{
			verifyValues([infiniteStream take:0], @[]);
			verifyValues([infiniteStream take:1], @[ RACUnit.defaultUnit ]);
			verifyValues([infiniteStream take:2], @[ RACUnit.defaultUnit, RACUnit.defaultUnit ]);
		});

		it(@"should take and terminate from a single-item stream", ^{
			NSArray *values = @[ RACUnit.defaultUnit ];
			RACStream *stream = streamWithValues(values);
			verifyValues([stream take:1], values);
		});
	});
  
	describe(@"zip stream creation methods", ^{
		__block NSArray *valuesOne;

		__block RACStream *streamOne;
		__block RACStream *streamTwo;
		__block RACStream *streamThree;
		__block NSArray *threeStreams;

		__block NSArray *oneStreamTuples;
		__block NSArray *twoStreamTuples;
		__block NSArray *threeStreamTuples;
		
		before(^{
			valuesOne = @[ @"Ada", @"Bob", @"Dea" ];
			NSArray *valuesTwo = @[ @"eats", @"cooks", @"jumps" ];
			NSArray *valuesThree = @[ @"fish", @"bear", @"rock" ];

			streamOne = streamWithValues(valuesOne);
			streamTwo = streamWithValues(valuesTwo);
			streamThree = streamWithValues(valuesThree);
			threeStreams = @[ streamOne, streamTwo, streamThree ];

			oneStreamTuples = @[
				RACTuplePack(valuesOne[0]),
				RACTuplePack(valuesOne[1]),
				RACTuplePack(valuesOne[2]),
			];

			twoStreamTuples = @[
				RACTuplePack(valuesOne[0], valuesTwo[0]),
				RACTuplePack(valuesOne[1], valuesTwo[1]),
				RACTuplePack(valuesOne[2], valuesTwo[2]),
			];

			threeStreamTuples = @[
				RACTuplePack(valuesOne[0], valuesTwo[0], valuesThree[0]),
				RACTuplePack(valuesOne[1], valuesTwo[1], valuesThree[1]),
				RACTuplePack(valuesOne[2], valuesTwo[2], valuesThree[2]),
			];
		});

		describe(@"-zipWith:", ^{
			it(@"should make a stream of tuples", ^{
				RACStream *stream = [streamOne zipWith:streamTwo];
				verifyValues(stream, twoStreamTuples);
			});
			
			it(@"should truncate streams", ^{
				RACStream *shortStream = streamWithValues(@[ @"now", @"later" ]);
				RACStream *stream = [streamOne zipWith:shortStream];

				verifyValues(stream, @[
					RACTuplePack(valuesOne[0], @"now"),
					RACTuplePack(valuesOne[1], @"later")
				]);
			});
			
			it(@"should work on infinite streams", ^{
				RACStream *stream = [streamOne zipWith:infiniteStream];
				verifyValues(stream, @[
					RACTuplePack(valuesOne[0], RACUnit.defaultUnit),
					RACTuplePack(valuesOne[1], RACUnit.defaultUnit),
					RACTuplePack(valuesOne[2], RACUnit.defaultUnit)
				]);
			});
			
			it(@"should handle multiples of the same stream", ^{
				RACStream *stream = [streamOne zipWith:streamOne];
				verifyValues(stream, @[
					RACTuplePack(valuesOne[0], valuesOne[0]),
					RACTuplePack(valuesOne[1], valuesOne[1]),
					RACTuplePack(valuesOne[2], valuesOne[2]),
				]);
			});
		});
		
		describe(@"+zip:reduce:", ^{
			it(@"should reduce values", ^{
				RACStream *stream = [streamClass zip:threeStreams reduce:^ NSString * (id x, id y, id z) {
					return [NSString stringWithFormat:@"%@ %@ %@", x, y, z];
				}];
				verifyValues(stream, @[ @"Ada eats fish", @"Bob cooks bear", @"Dea jumps rock" ]);
			});
			
			it(@"should truncate streams", ^{
				RACStream *shortStream = streamWithValues(@[ @"now", @"later" ]);
				NSArray *streams = [threeStreams arrayByAddingObject:shortStream];
				RACStream *stream = [streamClass zip:streams reduce:^ NSString * (id w, id x, id y, id z) {
					return [NSString stringWithFormat:@"%@ %@ %@ %@", w, x, y, z];
				}];
				verifyValues(stream, @[ @"Ada eats fish now", @"Bob cooks bear later" ]);
			});
			
			it(@"should work on infinite streams", ^{
				NSArray *streams = [threeStreams arrayByAddingObject:infiniteStream];
				RACStream *stream = [streamClass zip:streams reduce:^ NSString * (id w, id x, id y, id z) {
					return [NSString stringWithFormat:@"%@ %@ %@", w, x, y];
				}];
				verifyValues(stream, @[ @"Ada eats fish", @"Bob cooks bear", @"Dea jumps rock" ]);
			});
			
			it(@"should handle multiples of the same stream", ^{
				NSArray *streams = @[ streamOne, streamOne, streamTwo, streamThree, streamTwo, streamThree ];
				RACStream *stream = [streamClass zip:streams reduce:^ NSString * (id x1, id x2, id y1, id z1, id y2, id z2) {
					return [NSString stringWithFormat:@"%@ %@ %@ %@ %@ %@", x1, x2, y1, z1, y2, z2];
				}];
				verifyValues(stream, @[ @"Ada Ada eats fish eats fish", @"Bob Bob cooks bear cooks bear", @"Dea Dea jumps rock jumps rock" ]);
			});
		});
		
		describe(@"+zip:", ^{
			it(@"should make a stream of tuples out of single value", ^{
				RACStream *stream = [streamClass zip:@[ streamOne ]];
				verifyValues(stream, oneStreamTuples);
			});

			it(@"should make a stream of tuples out of an array of streams", ^{
				RACStream *stream = [streamClass zip:threeStreams];
				verifyValues(stream, threeStreamTuples);
			});

			it(@"should make an empty stream if given an empty array", ^{
				RACStream *stream = [streamClass zip:@[]];
				verifyValues(stream, @[]);
			});
			
			it(@"should make a stream of tuples out of an enumerator of streams", ^{
				RACStream *stream = [streamClass zip:threeStreams.objectEnumerator];
				verifyValues(stream, threeStreamTuples);
			});
			
			it(@"should make an empty stream if given an empty enumerator", ^{
				RACStream *stream = [streamClass zip:@[].objectEnumerator];
				verifyValues(stream, @[]);
			});
		});
	});

	describe(@"+concat:", ^{
		__block NSArray *streams = nil;
		__block NSArray *result = nil;
		
		before(^{
			RACStream *a = [streamClass return:@0];
			RACStream *b = [streamClass empty];
			RACStream *c = streamWithValues(@[ @1, @2, @3 ]);
			RACStream *d = [streamClass return:@4];
			RACStream *e = [streamClass return:@5];
			RACStream *f = [streamClass empty];
			RACStream *g = [streamClass empty];
			RACStream *h = streamWithValues(@[ @6, @7 ]);
			streams = @[ a, b, c, d, e, f, g, h ];
			result = @[ @0, @1, @2, @3, @4, @5, @6, @7 ];
		});
		
		it(@"should concatenate an array of streams", ^{
			RACStream *stream = [streamClass concat:streams];
			verifyValues(stream, result);
		});
		
		it(@"should concatenate an enumerator of streams", ^{
			RACStream *stream = [streamClass concat:streams.objectEnumerator];
			verifyValues(stream, result);
		});
	});

	it(@"should scan", ^{
		RACStream *stream = streamWithValues(@[ @1, @2, @3, @4 ]);
		RACStream *scanned = [stream scanWithStart:@0 reduce:^(NSNumber *running, NSNumber *next) {
			return @(running.integerValue + next.integerValue);
		}];

		verifyValues(scanned, @[ @1, @3, @6, @10 ]);
	});

	describe(@"taking with a predicate", ^{
		NSArray *values = @[ @0, @1, @2, @3, @0, @2, @4 ];

		__block RACStream *stream;

		before(^{
			stream = streamWithValues(values);
		});

		it(@"should take until a predicate is true", ^{
			RACStream *taken = [stream takeUntilBlock:^ BOOL (NSNumber *x) {
				return x.integerValue >= 3;
			}];

			verifyValues(taken, @[ @0, @1, @2 ]);
		});

		it(@"should take while a predicate is true", ^{
			RACStream *taken = [stream takeWhileBlock:^ BOOL (NSNumber *x) {
				return x.integerValue <= 1;
			}];

			verifyValues(taken, @[ @0, @1 ]);
		});

		it(@"should take a full stream", ^{
			RACStream *taken = [stream takeWhileBlock:^ BOOL (NSNumber *x) {
				return x.integerValue <= 10;
			}];

			verifyValues(taken, values);
		});

		it(@"should return an empty stream", ^{
			RACStream *taken = [stream takeWhileBlock:^ BOOL (NSNumber *x) {
				return x.integerValue < 0;
			}];

			verifyValues(taken, @[]);
		});

		it(@"should terminate an infinite stream", ^{
			RACStream *infiniteCounter = [infiniteStream scanWithStart:@0 reduce:^(NSNumber *running, id _) {
				return @(running.unsignedIntegerValue + 1);
			}];

			RACStream *taken = [infiniteCounter takeWhileBlock:^ BOOL (NSNumber *x) {
				return x.integerValue <= 5;
			}];

			verifyValues(taken, @[ @1, @2, @3, @4, @5 ]);
		});
	});

	describe(@"skipping with a predicate", ^{
		NSArray *values = @[ @0, @1, @2, @3, @0, @2, @4 ];

		__block RACStream *stream;

		before(^{
			stream = streamWithValues(values);
		});

		it(@"should skip until a predicate is true", ^{
			RACStream *taken = [stream skipUntilBlock:^ BOOL (NSNumber *x) {
				return x.integerValue >= 3;
			}];

			verifyValues(taken, @[ @3, @0, @2, @4 ]);
		});

		it(@"should skip while a predicate is true", ^{
			RACStream *taken = [stream skipWhileBlock:^ BOOL (NSNumber *x) {
				return x.integerValue <= 1;
			}];

			verifyValues(taken, @[ @2, @3, @0, @2, @4 ]);
		});

		it(@"should skip a full stream", ^{
			RACStream *taken = [stream skipWhileBlock:^ BOOL (NSNumber *x) {
				return x.integerValue <= 10;
			}];

			verifyValues(taken, @[]);
		});

		it(@"should finish skipping immediately", ^{
			RACStream *taken = [stream skipWhileBlock:^ BOOL (NSNumber *x) {
				return x.integerValue < 0;
			}];

			verifyValues(taken, values);
		});
	});

	describe(@"-combinePreviousWithStart:reduce:", ^{
		NSArray *values = @[ @1, @2, @3 ];
		__block RACStream *stream;
		beforeEach(^{
			stream = streamWithValues(values);
		});

		it(@"should pass the previous next into the reduce block", ^{
			NSMutableArray *previouses = [NSMutableArray array];
			RACStream *mapped = [stream combinePreviousWithStart:nil reduce:^(id previous, id next) {
				[previouses addObject:previous ?: RACTupleNil.tupleNil];
				return next;
			}];

			verifyValues(mapped, @[ @1, @2, @3 ]);

			NSArray *expected = @[ RACTupleNil.tupleNil, @1, @2 ];
			expect(previouses).to.equal(expected);
		});

		it(@"should send the combined value", ^{
			RACStream *mapped = [stream combinePreviousWithStart:@1 reduce:^(NSNumber *previous, NSNumber *next) {
				return [NSString stringWithFormat:@"%lu - %lu", (unsigned long)previous.unsignedIntegerValue, (unsigned long)next.unsignedIntegerValue];
			}];

			verifyValues(mapped, @[ @"1 - 1", @"1 - 2", @"2 - 3" ]);
		});
	});

	it(@"should reduce tuples", ^{
		RACStream *stream = streamWithValues(@[
			RACTuplePack(@"foo", @"bar"),
			RACTuplePack(@"buzz", @"baz"),
			RACTuplePack(@"", @"_")
		]);

		RACStream *reduced = [stream reduceEach:^(NSString *a, NSString *b) {
			return [a stringByAppendingString:b];
		}];

		verifyValues(reduced, @[ @"foobar", @"buzzbaz", @"_" ]);
	});
});

SharedExampleGroupsEnd
