//
//  TNLPseudoRequestOperationTest.m
//  TwitterNetworkLayer
//
//  Created on 10/29/14.
//  Copyright © 2020 Twitter. All rights reserved.
//

#import "NSDictionary+TNLAdditions.h"
#import "TNLError.h"
#import "TNLHTTPRequest.h"
#import "TNLPseudoURLProtocol.h"
#import "TNLRequestDelegate.h"
#import "TNLRequestOperationCancelSource.h"
#import "TNLRequestOperationQueue.h"
#import "TNLRequestRetryPolicyProvider.h"
#import "TNLResponse.h"

@import XCTest;

#define ENABLE_PSEUDO_REQUEST_TESTS 1

#if ENABLE_PSEUDO_REQUEST_TESTS

#define ENABLE_TIMING_TESTS 0 // timing is not reliable on CI machines

@interface TNLPseudoRequestOperationTest : XCTestCase <TNLRequestRetryPolicyProvider, TNLRequestDelegate>
@end

#define PSEUDO_ORIGIN @"http://www.pseudo.com"
#define PSEUDO_REDIRECT PSEUDO_ORIGIN @"/redirect"
#if ENABLE_TIMING_TESTS
#define TIME_BUFFER (0.25)
#endif

static TNLRequestOperationQueue *sQueue;
static TNLRequestConfiguration *sConfig;
static NSURL *sURL;
static NSHTTPURLResponse *sResponse;
static NSData *sData;

@implementation TNLPseudoRequestOperationTest

+ (void)setUp
{
    sURL = [NSURL URLWithString:PSEUDO_ORIGIN];
    sData = [@"{ garbage : \"data\" }" dataUsingEncoding:NSUTF8StringEncoding];
    sResponse = [[NSHTTPURLResponse alloc] initWithURL:sURL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Header1" : @"Value1" }];

    TNLMutableRequestConfiguration *config = [TNLMutableRequestConfiguration defaultConfiguration];
    config.protocolOptions = TNLRequestProtocolOptionPseudo;
    sConfig = [config copy];

    sQueue = [[TNLRequestOperationQueue alloc] initWithIdentifier:@"pseudo.request.test.queue"];
}

+ (void)tearDown
{
    [TNLPseudoURLProtocol unregisterEndpoint:sURL];
    sQueue = nil;
    sConfig = nil;
    sURL = nil;
    sData = nil;
    sResponse = nil;
}

- (void)tearDown
{
    [TNLPseudoURLProtocol unregisterAllEndpoints];
    [super tearDown];
}

- (void)registerCannedResponseWithConfig:(TNLPseudoURLResponseConfig *)config
{
    [TNLPseudoURLProtocol registerURLResponse:sResponse body:sData config:config withEndpoint:sURL];
}

- (void)registerRedirectWithBehavior:(TNLPseudoURLProtocolRedirectBehavior)behavior
{
    NSURL *redirectURL = [NSURL URLWithString:PSEUDO_REDIRECT];
    NSHTTPURLResponse *redirectResponse = [[NSHTTPURLResponse alloc] initWithURL:redirectURL
                                                                      statusCode:302
                                                                     HTTPVersion:@"HTTP/1.1"
                                                                    headerFields:@{ @"Location" : PSEUDO_ORIGIN }];
    TNLPseudoURLResponseConfig *config = [[TNLPseudoURLResponseConfig alloc] init];
    config.redirectBehavior = behavior;
    [TNLPseudoURLProtocol registerURLResponse:redirectResponse body:nil config:config withEndpoint:redirectURL];
}

#pragma mark Tests

- (void)testOperation200
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    [self registerCannedResponseWithConfig:nil];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // 200

    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    XCTAssertEqual(200, response.info.statusCode);
    XCTAssertTrue(response.info.data.length > 0);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 0.0, TIME_BUFFER);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperation200_TempSuspension
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    [self registerCannedResponseWithConfig:nil];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Temprorary Suspend

    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue suspend];
    [sQueue enqueueRequestOperation:op];
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.1]];
    [sQueue resume];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    XCTAssertEqual(200, response.info.statusCode);
    XCTAssertTrue(response.info.data.length > 0);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 1.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 1.0, TIME_BUFFER);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperation200_Delay
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Delay

    pseudoConfig.delay = 1000;
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    XCTAssertEqual(200, response.info.statusCode);
    XCTAssertTrue(response.info.data.length > 0);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 1.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 1.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 1.0, TIME_BUFFER);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperation200_Delay_and_Latency
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Latency + Delay

    pseudoConfig.delay = 1000;
    pseudoConfig.latency = 250;
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    XCTAssertEqual(200, response.info.statusCode);
    XCTAssertTrue(response.info.data.length > 0);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 1.5, TIME_BUFFER * 2);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 1.5, TIME_BUFFER * 2);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 1.5, TIME_BUFFER * 2);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperation200_Latency
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Latency

    pseudoConfig.latency = 250;
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    XCTAssertEqual(200, response.info.statusCode);
    XCTAssertTrue(response.info.data.length > 0);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 0.5, TIME_BUFFER * 2);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 0.5, TIME_BUFFER * 2);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 0.5, TIME_BUFFER * 2);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperation302_Follow
{
    NSURL *redirectURL = [NSURL URLWithString:PSEUDO_REDIRECT];

    [self registerRedirectWithBehavior:TNLPseudoURLProtocolRedirectBehaviorFollowLocation];
    [self registerCannedResponseWithConfig:nil];

    TNLMutableHTTPRequest *request = [[TNLMutableHTTPRequest alloc] initWithURL:redirectURL];
    TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request configuration:sConfig delegate:self];

    // expect redirection

    __block NSURLRequest *fromRequest;
    __block NSURLRequest *toRequest;
    XCTestExpectation *expectRedirect;
    expectRedirect = [self expectationForNotification:@"Redirect" object:op handler:^BOOL(NSNotification *notification) {
        fromRequest = notification.userInfo[@"fromRequest"];
        toRequest = notification.userInfo[@"toRequest"];
        return YES;
    }];

    // expect completion

    __block TNLResponse *response;
    XCTestExpectation *expectComplete;
    expectComplete = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    // enqueue the operation and wait for it to finish

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    // check that the redirect happend correctly

    XCTAssertEqualObjects(PSEUDO_REDIRECT, fromRequest.URL.absoluteString);
    XCTAssertEqualObjects(PSEUDO_ORIGIN, toRequest.URL.absoluteString);
    XCTAssertEqualObjects(sURL, toRequest.URL);
    XCTAssertEqual(response.metrics.attemptCount, (NSUInteger)2);
    XCTAssertEqual(response.metrics.attemptMetrics.firstObject.attemptType, TNLAttemptTypeInitial);
    XCTAssertEqual(response.metrics.attemptMetrics.lastObject.attemptType, TNLAttemptTypeRedirect);

    // check that the completion happened correctly

    XCTAssertEqual(200, response.info.statusCode);
    XCTAssertGreaterThan(response.info.data.length,  0);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqualObjects(response, op.response);
}

- (void)testOperation302_FollowIfRegistered
{
    NSURL *redirectURL = [NSURL URLWithString:PSEUDO_REDIRECT];

    [self registerRedirectWithBehavior:TNLPseudoURLProtocolRedirectBehaviorFollowLocationIfRedirectResponseIsRegistered];
    [self registerCannedResponseWithConfig:nil];

    TNLMutableHTTPRequest *request = [[TNLMutableHTTPRequest alloc] initWithURL:redirectURL];
    TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request configuration:sConfig delegate:self];

    // expect redirection

    __block NSURLRequest *fromRequest;
    __block NSURLRequest *toRequest;
    XCTestExpectation *expectRedirect;
    expectRedirect = [self expectationForNotification:@"Redirect" object:op handler:^BOOL(NSNotification *notification) {
        fromRequest = notification.userInfo[@"fromRequest"];
        toRequest = notification.userInfo[@"toRequest"];
        return YES;
    }];

    // expect completion

    __block TNLResponse *response;
    XCTestExpectation *expectComplete;
    expectComplete = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    // enqueue the operation and wait for it to finish

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    // check that the redirect happend correctly

    XCTAssertEqualObjects(PSEUDO_REDIRECT, fromRequest.URL.absoluteString);
    XCTAssertEqualObjects(PSEUDO_ORIGIN, toRequest.URL.absoluteString);
    XCTAssertEqualObjects(sURL, toRequest.URL);
    XCTAssertEqual(response.metrics.attemptCount, (NSUInteger)2);
    XCTAssertEqual(response.metrics.attemptMetrics.firstObject.attemptType, TNLAttemptTypeInitial);
    XCTAssertEqual(response.metrics.attemptMetrics.lastObject.attemptType, TNLAttemptTypeRedirect);

    // check that the completion happened correctly

    XCTAssertEqual(200, response.info.statusCode);
    XCTAssertGreaterThan(response.info.data.length,  0);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqualObjects(response, op.response);
}

- (void)testOperation302_DontFollowIfNotRegistered
{
    NSURL *redirectURL = [NSURL URLWithString:PSEUDO_REDIRECT];

    [self registerRedirectWithBehavior:TNLPseudoURLProtocolRedirectBehaviorFollowLocationIfRedirectResponseIsRegistered];

    TNLMutableHTTPRequest *request = [[TNLMutableHTTPRequest alloc] initWithURL:redirectURL];
    TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request configuration:sConfig delegate:self];

    // expect completion

    __block TNLResponse *response;
    XCTestExpectation *expectComplete;
    expectComplete = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    // enqueue the operation and wait for it to finish

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    // check that the redirect did not happen

    XCTAssertEqual(response.metrics.attemptCount, (NSUInteger)1);
    XCTAssertEqual(response.metrics.attemptMetrics.firstObject.attemptType, TNLAttemptTypeInitial);

    // check that the completion happened correctly

    XCTAssertEqual(302, response.info.statusCode);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqualObjects(response, op.response);
    XCTAssertEqualObjects([response.info valueForResponseHeaderField:@"Location"], PSEUDO_ORIGIN);
}

- (void)testOperation302_DontFollow
{
    NSURL *redirectURL = [NSURL URLWithString:PSEUDO_REDIRECT];

    [self registerRedirectWithBehavior:TNLPseudoURLProtocolRedirectBehaviorDontFollowLocation];
    [self registerCannedResponseWithConfig:nil];

    TNLMutableHTTPRequest *request = [[TNLMutableHTTPRequest alloc] initWithURL:redirectURL];
    TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request configuration:sConfig delegate:self];

    // expect completion

    __block TNLResponse *response;
    XCTestExpectation *expectComplete;
    expectComplete = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    // enqueue the operation and wait for it to finish

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    // check that the redirect did not happen

    XCTAssertEqual(response.metrics.attemptCount, (NSUInteger)1);
    XCTAssertEqual(response.metrics.attemptMetrics.firstObject.attemptType, TNLAttemptTypeInitial);

    // check that the completion happened correctly

    XCTAssertEqual(302, response.info.statusCode);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqualObjects(response, op.response);
    XCTAssertEqualObjects([response.info valueForResponseHeaderField:@"Location"], PSEUDO_ORIGIN);
}

- (void)testOperationError_AttemptTimeout
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Attempt Timeout (own by NSURL stack and uses a fairly sizeable leeway)

    mConfig.attemptTimeout = 1.0;
    mConfig.operationTimeout = 4.0;
    pseudoConfig.latency = 1500;
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateFailed);

    XCTAssertEqualObjects(response.operationError.domain, TNLErrorDomain);
    XCTAssertEqual(response.operationError.code, TNLErrorCodeRequestOperationAttemptTimedOut);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 1.0, 1.0);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 1.0, 1.0);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 1.0, 1.0);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperationError_OperationTimeout
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Operation Timeout

    mConfig.attemptTimeout = 1.0;
    mConfig.operationTimeout = 2.0;
    mConfig.retryPolicyProvider = self;
    pseudoConfig.latency = 1500;
    // [mRequest setValue:@"2000" forHTTPHeaderField:@"RETRY_DELAY"];
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateFailed);

    XCTAssertEqualObjects(response.operationError.domain, TNLErrorDomain);
    XCTAssertEqual(response.operationError.code, TNLErrorCodeRequestOperationOperationTimedOut);
    XCTAssertEqual(response, op.response, @"%@ vs %@", response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 1.5, 1.0);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 1.5, 1.0);
#endif
    XCTAssertEqual(2UL, response.metrics.attemptCount);
}

- (void)testOperationError_OperationTimeout2
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Operation Timeout 2

    mConfig.attemptTimeout = 1.0;
    mConfig.operationTimeout = 2.0;
    mConfig.retryPolicyProvider = self;
    pseudoConfig.latency = 1500;
    [mRequest setValue:@"2000" forHTTPHeaderField:@"RETRY_DELAY"];
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateFailed);

    XCTAssertNotEqual(response.operationError.code, TNLErrorCodeRequestOperationOperationTimedOut);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 1.5, TIME_BUFFER * 2);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 1.5, TIME_BUFFER * 2);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 1.5, TIME_BUFFER * 2);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperationError_OperationTimeout3
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Operation Timeout 3

    pseudoConfig.latency = 1500;
    [mRequest removeAllValuesForHTTPHeaderField:@"RETRY_DELAY"];
    mConfig.attemptTimeout = 3.0;
    mConfig.operationTimeout = 1.2;
    mConfig.retryPolicyProvider = self;
    // [mRequest setValue:@"2000" forHTTPHeaderField:@"RETRY_DELAY"];
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateFailed);

    XCTAssertEqualObjects(response.operationError.domain, TNLErrorDomain);
    XCTAssertEqual(response.operationError.code, TNLErrorCodeRequestOperationOperationTimedOut);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 1.2, TIME_BUFFER + .1);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 1.2, TIME_BUFFER + .1);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 1.2, TIME_BUFFER + .1);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperationError_Disconnect
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;
    NSError *expectedError = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorNetworkConnectionLost userInfo:nil];

    // Disconnect

    pseudoConfig.failureError = expectedError;
    pseudoConfig.delay = 1000;
    pseudoConfig.latency = 250;
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateFailed);

    XCTAssertEqual(0, response.info.statusCode);
    XCTAssertNotNil(response.operationError);
    XCTAssertEqualObjects(expectedError.domain, response.operationError.domain);
    XCTAssertEqual(expectedError.code, response.operationError.code);
    XCTAssertEqual(response, op.response);
}

- (void)testOperation404
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // 404

    pseudoConfig.statusCode = 404;
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateSucceeded);

    XCTAssertEqual(404, response.info.statusCode);
    XCTAssertTrue(response.info.data.length > 0);
    XCTAssertEqual(1.0, op.downloadProgress);
    XCTAssertEqual(1.0, op.uploadProgress);
    XCTAssertNil(response.operationError);
    XCTAssertEqual(response, op.response);

#if ENABLE_TIMING_TESTS
    XCTAssertEqualWithAccuracy(response.metrics.queuedDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.allAttemptsDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.currentAttemptDuration, 0.0, TIME_BUFFER);
    XCTAssertEqualWithAccuracy(response.metrics.totalDuration, 0.0, TIME_BUFFER);
#endif
    XCTAssertEqual(1UL, response.metrics.attemptCount);
}

- (void)testOperation404_Cancel
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];
    TNLPseudoURLResponseConfig *pseudoConfig = [[TNLPseudoURLResponseConfig alloc] init];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Cancel

    pseudoConfig.delay = 1000;
    pseudoConfig.statusCode = 404;
    mConfig.retryPolicyProvider = self;
    [self registerCannedResponseWithConfig:pseudoConfig];
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];

    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.5]];
    [op cancelWithSource:@"FORCE_CANCEL_SOURCE"];

    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertTrue(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateCancelled);

    XCTAssertEqual(0, response.info.statusCode);
    XCTAssertNotNil(response.operationError);
    XCTAssertEqualObjects(response.operationError.domain, TNLErrorDomain);
    XCTAssertEqual(response.operationError.code, TNLErrorCodeRequestOperationCancelled);
    XCTAssertEqualObjects(response.operationError.userInfo[TNLErrorCancelSourceKey], @"FORCE_CANCEL_SOURCE");
    XCTAssertEqual(response, op.response);
}

- (void)testOperation404_EarlyCancel1
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Early Cancel 1

    mConfig.retryPolicyProvider = self;
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [op cancelWithSource:@"FORCE_CANCEL_SOURCE"];
    [sQueue enqueueRequestOperation:op];

    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertTrue(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateCancelled);

    XCTAssertEqual(0, response.info.statusCode);
    XCTAssertNotNil(response.operationError);
    XCTAssertEqualObjects(response.operationError.domain, TNLErrorDomain);
    XCTAssertEqual(response.operationError.code, TNLErrorCodeRequestOperationCancelled);
    XCTAssertEqualObjects(response.operationError.userInfo[TNLErrorCancelSourceKey], @"FORCE_CANCEL_SOURCE");
    XCTAssertEqual(response, op.response);
}

- (void)testOperation404_EarlyCancel2
{
    TNLMutableRequestConfiguration *mConfig = [sConfig mutableCopy];
    TNLMutableHTTPRequest *mRequest = [[TNLMutableHTTPRequest alloc] initWithURL:sURL];

    __block TNLResponse *response;
    TNLRequestOperation *op;
    XCTestExpectation *expect;

    // Early Cancel 2

    mConfig.retryPolicyProvider = self;
    op = [TNLRequestOperation operationWithRequest:mRequest configuration:mConfig delegate:self];
    expect = [self expectationForNotification:@"Complete" object:op handler:^BOOL(NSNotification *notification) {
        response = notification.userInfo[@"response"];
        return YES;
    }];

    XCTAssertFalse(op.isCancelled);
    XCTAssertFalse(op.isFinished);
    XCTAssertFalse(op.isExecuting);
    XCTAssertEqual(op.state, TNLRequestOperationStateIdle);

    [sQueue enqueueRequestOperation:op];
    [op cancelWithSource:@"FORCE_CANCEL_SOURCE"];

    [self waitForExpectationsWithTimeout:5.0 handler:nil];

    XCTAssertTrue(op.isCancelled);
    XCTAssertFalse(op.isExecuting);
    XCTAssertTrue(op.isFinished);
    XCTAssertEqual(op.state, TNLRequestOperationStateCancelled);

    XCTAssertEqual(0, response.info.statusCode);
    XCTAssertNotNil(response.operationError);
    XCTAssertEqualObjects(response.operationError.domain, TNLErrorDomain);
    XCTAssertEqual(response.operationError.code, TNLErrorCodeRequestOperationCancelled);
    XCTAssertEqualObjects(response.operationError.userInfo[TNLErrorCancelSourceKey], @"FORCE_CANCEL_SOURCE");
    XCTAssertEqual(response, op.response);
}

#pragma mark Retry Policy

- (BOOL)tnl_shouldRetryRequestOperation:(TNLRequestOperation *)op withResponse:(TNLResponse *)response
{
    return response.info.statusCode != 200;
}

- (NSTimeInterval)tnl_delayBeforeRetryForRequestOperation:(TNLRequestOperation *)op withResponse:(TNLResponse *)response
{
    return [[op.originalRequest.allHTTPHeaderFields tnl_objectForCaseInsensitiveKey:@"RETRY_DELAY"] integerValue];
}

#pragma mark TNLRequestDelegate

- (void)tnl_requestOperation:(TNLRequestOperation *)op didRedirectFromURLRequest:(NSURLRequest *)fromRequest toURLRequest:(NSURLRequest *)toRequest
{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"Redirect" object:op userInfo:@{ @"fromRequest" : fromRequest, @"toRequest" : toRequest }];
}

- (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteWithResponse:(TNLResponse *)response
{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"Complete" object:op userInfo:@{ @"response" : response }];
}

- (void)tnl_requestOperation:(TNLRequestOperation *)op hydrateRequest:(id<TNLRequest>)request completion:(TNLRequestHydrateCompletionBlock)complete
{
    complete(request, nil);
}

@end

#endif // ENABLE_PSEUDO_REQUEST_TESTS
