/*
 *  Author: Sergei Golovin <Golovin.SV@gmail.com>
 */

#import "NSURLConnectionTest.h"

/* the runloop's time slice */
#define TIMING 0.1
/* the max duration of a test */
#define MAXDURATION 3.0

@interface NSURLConnectionTest (Private)

/**
 *  The method tries to guess the request which the test case should make.
 *  It recognizes as an argument an NSDictionary with custom variable parts
 *  of connecting process (see the class's description). It also can be supplied
 *  with the exact request which must be made during test execution.
 *  The result of the method is stored in the ivar _request.
 */
- (void)_makeRequest:(id)extra;

/**
 *  The method analyzes the ivar _request and tries to guess the execution path
 *  inferring from it the reference flag set. If there is a deviation from the guessed
 *  execution path the caller can supply NSDictionary as the argument 'extra' with
 *  the key 'ReferenceFlags' is set to have a custom NSDictionary as a value explicitly
 *  setting the particular flags (see the class's description).
 */
- (void)_makeReferenceFlags:(id)extra;

@end /* NSURLConnectionTest (Private) */

@implementation NSURLConnectionTest

+ (Class) testWebServerClass
{
  return [TestWebServer class];
}

/* The flag map connects flags's string names with flags... used to log actions */
static NSMapTable *_flagMap = nil;

+ (void)initialize
{
  if (nil == _flagMap)
    {
      _flagMap = NSCreateMapTable(NSObjectMapKeyCallBacks, 
				   NSIntegerMapValueCallBacks,
				   0);
      NSMapInsert(_flagMap, @"SENTREQUEST",     (void *)1);
      NSMapInsert(_flagMap, @"AUTHORIZED",      (void *)2);
      NSMapInsert(_flagMap, @"NOTAUTHORIZED",   (void *)4);
      NSMapInsert(_flagMap, @"GOTUNAUTHORIZED", (void *)8);
      NSMapInsert(_flagMap, @"GOTREQUEST",      (void *)16);
      NSMapInsert(_flagMap, @"SENTRESPONSE",    (void *)32);
      NSMapInsert(_flagMap, @"GOTRESPONSE",     (void *)64);
      NSMapInsert(_flagMap, @"GOTCONTENT",      (void *)128);
      NSMapInsert(_flagMap, @"GOTFINISH",       (void *)256);
      NSMapInsert(_flagMap, @"GOTFAIL",         (void *)512);
      NSMapInsert(_flagMap, @"GOTREDIRECT",     (void *)1024);
    }
}

- (id)init
{
  if ((self = [super init]) != nil)
    {
      _conn = nil;
      _error = nil;
      _server = nil;
      _auxServer = nil;
      _received = nil;
      _request = nil;
      _redirectRequest = nil;
      _expectedStatusCode = 204;
      _expectedContent = nil;
    }

  return self;
}

// super's -[dealloc] calls the -[tearDownTest:]
// so place clearing there

- (void)setUpTest:(id)extra
{
  [super setUpTest: extra];

  [self _makeRequest: extra];
  [self _makeReferenceFlags: extra];
  _received = [NSMutableData new];
}

- (void)startTest:(id)extra
{
  CREATE_AUTORELEASE_POOL(arp);
  NSTimeInterval duration = 0.0;

  if (YES == _debug)
    {
      NSLog(@"%@: started with request:\n%@", self, _request);
      [self logFlags];

      NSMutableSet	*s = [[NSProcessInfo processInfo] debugSet];
      [s addObject: @"NSURLConnection"];
      [s addObject: @"NSURLProtocol"];
      [s addObject: @"NSStream"];
      [s addObject: @"NSRunLoop"];
    }

  _conn = [[NSURLConnection alloc] initWithRequest: _request
					  delegate: self];

  while (!_done && !_failed)
    {
      [[NSRunLoop currentRunLoop]
		runUntilDate: [NSDate dateWithTimeIntervalSinceNow: TIMING]];
      duration += TIMING;
      if (duration >= MAXDURATION)
	{
	  _failed = YES;
	}
    }
  if (YES == _debug)
    {
      NSLog(@"%@: stopped with request:\n%@", self, _request);
      [self logFlags];
    }
  DESTROY(arp);
}

- (void)tearDownTest:(id)extra
{
  BOOL isToStop = YES; // whether to stop the main TestWebServer instance
  BOOL isToStopAux = YES; // whether to stop the auxilliary TestWebServer instance
  if ([_extra isKindOfClass: [NSDictionary class]])
    {
      NSDictionary *d = _extra;
      isToStop = ([d objectForKey: @"Instance"] == nil);
      isToStopAux = ([d objectForKey: @"AuxInstance"] == nil &&
		     [[d objectForKey: @"IsAuxilliary"] isEqualToString: @"YES"]);
    }

  if (isToStop)
    {
      if (nil != _server)
	{
	  [_server stop];
	}
    }
  if (isToStopAux)
    {
      if (nil != _auxServer)
	{
	  [_auxServer stop];
	}
    }

  DESTROY(_server);
  DESTROY(_auxServer);
  DESTROY(_received);
  DESTROY(_request);
  DESTROY(_redirectRequest);
  DESTROY(_expectedContent);
  [_conn cancel];
  DESTROY(_conn);
  DESTROY(_error);
  
  [super tearDownTest: extra];
}

- (BOOL)isSuccess
{
  BOOL ret = [super isSuccess];


  if (!ret)
    {
      /* log the flags that differ */
      NSString *key;
      NSUInteger flag;
      NSMapEnumerator en = NSEnumerateMapTable(_flagMap);
      while (NSNextMapEnumeratorPair(&en, (void **)&key, (void **)&flag))
	{
	  if ([self isReferenceFlagSet: flag] != [self isFlagSet: flag])
	    {
	      NSLog(@"ERR: %@", key);
	    }
	}
      NSEndMapTableEnumeration(&en);

      if (_failed)
	{
	  NSLog(@"FAILED for unknown reason possibly not related to the test (e.g. a timer has expired)");
	}
    }

  return ret;
}

- (void)logFlags
{
  NSString *value;
  NSString *key;
  NSUInteger flag;
  NSMapEnumerator en = NSEnumerateMapTable(_flagMap);
  while (NSNextMapEnumeratorPair(&en, (void **)&key, (void **)&flag))
    {
      if ([self isFlagSet: flag])
	{
	  value = @"YES";
	}
      else
	{
	  value = @"NO";
	}

      NSLog(@"ACTUAL:    %@ -> %@", key, value);

      if ([self isReferenceFlagSet: flag])
	{
	  value = @"YES";
	}
      else
	{
	  value = @"NO";
	}

      NSLog(@"REFERENCE: %@ -> %@", key, value);
    }
  NSEndMapTableEnumeration(&en);  
}

- (NSError*) error
{
  return _error;
}

/* TestWebServerDelegate */
- (void)handler:(id)handler
     gotRequest:(GSMimeDocument *)request
	   with:(TestWebServer *)server
{
  NSString *method = [_request HTTPMethod];
  NSData *content = [request convertToData];

  [self setFlags: SENTREQUEST];
  if (YES == _debug)
    {
      NSLog(@"%@: set SENTREQUEST (-[%@])", self, NSStringFromSelector(_cmd));
    }

  // TODO: more comparisons of _request and request
  if ([method isEqualToString: @"POST"] ||
     [method isEqualToString: @"PUT"])
    {
      if ([content isEqualToData: [_request HTTPBody]])
	{
	  [self setFlags: GOTREQUEST];
	  if (YES == _debug)
	    {
	      NSLog(@"%@: set GOTREQUEST (-[%@])", self, NSStringFromSelector(_cmd));
	    }
	}
    }
  else
    {
      [self setFlags: GOTREQUEST];
      if (YES == _debug)
	{
	  NSLog(@"%@: set GOTREQUEST (-[%@])", self, NSStringFromSelector(_cmd));
	}
    }
}

- (void)handler:(id)handler
willSendUnauthorized:(GSMimeDocument *)response
	   with:(TestWebServer *)server
{
  [self setFlags: NOTAUTHORIZED];
  if (YES == _debug)
    {
      NSLog(@"%@: set NOTAUTHORIZED (-[%@])", self, NSStringFromSelector(_cmd));
    }
}

- (void)handler:(id)handler
  gotAuthorized:(GSMimeDocument *)request
	   with:(TestWebServer *)server
{
  [self setFlags: AUTHORIZED];
  if (YES == _debug)
    {
      NSLog(@"%@: set AUTHORIZED (-[%@])", self, NSStringFromSelector(_cmd));
    }
}

- (void)handler:(id)handler
       willSend:(GSMimeDocument *)response
	   with:(TestWebServer *)server
{
  [self setFlags: SENTRESPONSE];
  if (YES == _debug)
    {
      NSLog(@"%@: set SENTRESPONSE (-[%@])", self, NSStringFromSelector(_cmd));
    }
}

- (void)timeoutExceededByHandler:(id)handler
{
  _failed = YES;
}
/* end of TestWebServerDelegate */

/* NSURLConnectionDelegate */
- (NSURLRequest *)connection:(NSURLConnection *)connection
             willSendRequest:(NSURLRequest *)request
            redirectResponse:(NSURLResponse *)redirectResponse
{
  if ([redirectResponse isKindOfClass: [NSHTTPURLResponse class]])
    {
      if ([(NSHTTPURLResponse *)redirectResponse statusCode] == _redirectStatusCode)
	{
	  [self setFlags: GOTREDIRECT];
	  if (YES == _debug)
	    {
	      NSLog(@"%@: set GOTREDIRECT (-[%@])", self, NSStringFromSelector(_cmd));
	    }
	}
    }
  else
    {
      [self setFlags: GOTREDIRECT];
      if (YES == _debug)
	{
	  NSLog(@"%@: set GOTREDIRECT (-[%@])", self, NSStringFromSelector(_cmd));
	}
    }

  return _redirectRequest;
}

- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
  [self setFlags: GOTUNAUTHORIZED];
  if (YES == _debug)
    {
      NSLog(@"%@: set GOTUNAUTHORIZED (-[%@])", self, NSStringFromSelector(_cmd));
    }

  if ([challenge previousFailureCount] == 0)
    {
      NSURLCredential *cred;
      // TODO: _auxServer?
      NSString *login = [_server login];
      NSString *password = [_server password];

      if (nil == login)
	{
	  if ([_extra isKindOfClass: [NSDictionary class]])
	    {
	      login = [(NSDictionary *)_extra objectForKey: @"Login"];
	    }
	}
      if (nil == password)
	{
	  if ([_extra isKindOfClass: [NSDictionary class]])
	    {
	      password = [(NSDictionary *)_extra objectForKey: @"Password"];
	    }
	}
      cred = [NSURLCredential credentialWithUser: login
					password: password
				     persistence: NSURLCredentialPersistenceNone];
      [[challenge sender] useCredential: cred
	     forAuthenticationChallenge: challenge];
    }
  else
    {
      [[challenge sender] cancelAuthenticationChallenge: challenge];
    }
}

- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
  if (YES == _debug)
    {
      if ([response isKindOfClass: [NSHTTPURLResponse class]])
	{
	  NSLog(@"%@: received response (-[%@]):\nStatus Code: %i",
            self, NSStringFromSelector(_cmd),
            (int)[(NSHTTPURLResponse *)response statusCode]);
	}
    }

  if ([response isKindOfClass: [NSHTTPURLResponse class]])
    {
      if ([(NSHTTPURLResponse *)response statusCode] == _expectedStatusCode)
	{
	  [self setFlags: GOTRESPONSE];
	  if (YES == _debug)
	    {
	      NSLog(@"%@: set GOTRESPONSE (-[%@])", self, NSStringFromSelector(_cmd));
	    }
	}
    }
  else
    {
      [self setFlags: GOTRESPONSE];
      if (YES == _debug)
	{
	  NSLog(@"%@: set GOTRESPONSE (-[%@])", self, NSStringFromSelector(_cmd));
	}
    }
}

- (void)connection:(NSURLConnection *)connection
    didReceiveData:(NSData *)data
{
  [_received appendData: data];
  if (YES == _debug)
    {
      NSLog(@"%@: received data '%@' (-[%@])", self, data, NSStringFromSelector(_cmd));
    }
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
  if (nil != _expectedContent &&
     [_received isEqualToData: _expectedContent])
    {
      [self setFlags: GOTCONTENT];
      if (YES == _debug)
	{
	  NSLog(@"%@: set GOTCONTENT (-[%@])", self, NSStringFromSelector(_cmd));
	}
    }

  [self setFlags: GOTFINISH];
  if (YES == _debug)
    {
      NSLog(@"%@: set GOTFINISH (-[%@])", self, NSStringFromSelector(_cmd));
    }

  _done = YES;
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
  ASSIGN(_error, error);

  /*  if ((nil != _expectedContent &&
      [_received isEqualToData: _expectedContent]) ||
     (nil == _expectedContent && [_received length] == 0))
    {
      [self setFlags: GOTCONTENT];
    }
  */

  [self setFlags: GOTFAIL];
  if (YES == _debug)
    {
      NSLog(@"%@: set GOTFAIL (-[%@])", self, NSStringFromSelector(_cmd));
      NSLog(@"%@: error %@", self, error);
    }

  _done = YES;
}

/* end of NSURLConnectionDelegate */

@end /* NSURLConnectionTest */

@implementation NSURLConnectionTest (Private)

- (void)_makeRequest:(id)extra
{
  BOOL isOwnServer = YES;
  BOOL isOwnAuxServer = NO;
  NSString *tmp;
  BOOL isDetached = NO;
  TestWebServer *instance = nil;
  TestWebServer *auxInstance = nil;
  NSString *protocol = nil;
  NSString *address = nil;
  NSString *auxPort = nil;
  NSString *port = nil;
  //  NSString *login = nil;
  //  NSString *password = nil;
  NSString *path = nil;
  NSString *redirectPath = nil;
  NSString *statusCode = nil;
  NSString *redirectCode = nil;
  NSString *method = nil;
  id payload = nil;
  id content = nil;
  NSURL *url;
  NSDictionary *d = nil;

  if ([extra isKindOfClass: [NSDictionary class]])
    {
      d = extra;

      instance = [d objectForKey: @"Instance"];
      if (nil != instance)
	{
	  NSString *value;

	  protocol = [instance isSecure] ? @"https" : @"http";
	  if ((value = [d objectForKey: @"Protocol"]) == nil ||
	     ![[value lowercaseString] isEqualToString: protocol])
	    {
	      d = [d mutableCopy];
	      [(NSMutableDictionary *)d setObject: protocol forKey: @"Protocol"];
	      [d autorelease];
	    }

	  address = [instance address];
	  port = [instance port];
	  ASSIGN(_server, instance);
	  [_server setDelegate: self];
	  [_server setDebug: _debug];
	  isOwnServer = NO;
	}
      else
	{
	  protocol = [[d objectForKey: @"Protocol"] lowercaseString];
	  address = [d objectForKey: @"Address"];
	  port = [d objectForKey: @"Port"];
	}

      auxInstance = [d objectForKey: @"AuxInstance"];
      if (nil != auxInstance)
	{
	  auxPort = [auxInstance port];
	  ASSIGN(_auxServer, auxInstance);
	  [_auxServer setDelegate: self];
	  [_auxServer setDebug: _debug];
	  isOwnAuxServer = NO;
	}

      if (isOwnServer)
	{
	  if ((tmp = [d objectForKey: @"IsDetached"]) != nil)
	    {
	      if ([tmp isEqualToString: @"YES"])
		{
		  isDetached = YES;
		}
	      else
		{
		  isDetached = NO;
		}
	    }
	}

      isOwnAuxServer
	= [[d objectForKey: @"IsAuxilliary"] isEqualToString: @"YES"]
	&& nil == _auxServer;
      if (isOwnAuxServer)
	{
	  auxPort = [d objectForKey: @"AuxPort"];
	}

      path = [d objectForKey: @"Path"];
      if (isOwnAuxServer || nil != _auxServer)
	{
	  redirectPath = [d objectForKey: @"RedirectPath"];
	}
      statusCode = [d objectForKey: @"StatusCode"];
      // TODO: conditions?
      redirectCode = [d objectForKey: @"RedirectCode"];
      method = [d objectForKey: @"Method"];
      payload = [d objectForKey: @"Payload"];
      content = [d objectForKey: @"Content"];
    } // Is extra NSDictionary?
  else if ([extra isKindOfClass: [NSURLRequest class]])
    {
      ASSIGN(_request, extra);
    }

  if (nil == _request)
    {
      if (nil == protocol)
	{
	  protocol = @"http";
	}
      if (nil == address)
	{
	  address = @"127.0.0.1";
	}
      if (nil == port)
	{
	  port = @"1234";
	}
      if (nil == auxPort)
	{
	  auxPort = @"1235";
	}
      if (nil == path)
	{
	  path = @"/";
	}
      if ((isOwnAuxServer || nil != _auxServer) && nil == redirectPath)
	{
	  redirectPath = path;
	}
      if (nil != redirectPath && nil == redirectCode)
	{
	  _redirectStatusCode = 301;
	}
      if (nil == statusCode)
	{
	  _expectedStatusCode = 204;
	}
      else
	{
	  _expectedStatusCode = [statusCode intValue];
	  if (_expectedStatusCode == 0)
	    {
	      [NSException raise: NSInvalidArgumentException
	  	format: @"Invalid expected 'StatusCode' supplied %@",
		statusCode];
	    }
	}
      if (nil == redirectCode)
	{
	  _redirectStatusCode = 301;
	}
      else
	{
	  _redirectStatusCode = [redirectCode intValue];
	  if (_redirectStatusCode == 0)
	    {
	      [NSException raise: NSInvalidArgumentException
		format: @"Invalid expected 'RedirectCode' supplied %@",
		redirectCode];
	    }
	}

      if (nil == method)
	{
	  method = @"GET";
	}

      if (![path hasPrefix: @"/"])
	{
	  // path MUST begin with '/'
	  path = [@"/" stringByAppendingString: path];
	}

      if (![redirectPath hasPrefix: @"/"])
	{
	  // path MUST begin with '/'
	  redirectPath = [@"/" stringByAppendingString: redirectPath];
	}

      url = [NSURL URLWithString: 
        [NSString stringWithFormat:
	  @"%@://%@:%@%@", protocol, address, auxPort, redirectPath]];
      _redirectRequest = [NSMutableURLRequest requestWithURL: url];
      RETAIN(_redirectRequest);

      url = [NSURL URLWithString: 
	[NSString stringWithFormat:
	  @"%@://%@:%@%@", protocol, address, port, path]];
      _request = [NSMutableURLRequest requestWithURL: url];
      RETAIN(_request);

      [(NSMutableURLRequest *)_request setHTTPMethod: method];
      if (nil != payload)
	{
	  if ([payload isKindOfClass: [NSString class]])
	    {
	      payload = [payload dataUsingEncoding: NSUTF8StringEncoding];
	    }
	  if (![payload isKindOfClass: [NSData class]])
	    {
	      [NSException raise: NSInvalidArgumentException
			  format: @"invalid payload"];
	    }
	  [(NSMutableURLRequest *)_request setHTTPBody: payload];
	}

      if (nil != content)
	{
	  if ([content isKindOfClass: [NSString class]])
	    {
	      content = [content dataUsingEncoding: NSUTF8StringEncoding];
	    }
	  if (![content isKindOfClass: [NSData class]])
	    {
	      [NSException raise: NSInvalidArgumentException
			  format: @"invalid content"];
	    }
	  ASSIGN(_expectedContent, content);
	}
      if (nil != _expectedContent && nil == statusCode)
	{
	  _expectedStatusCode = 200;
	}
    }

  if (isOwnServer)
    {
      _server = [[TestWebServer alloc] initWithAddress: address
						  port: port
						  mode: isDetached
						 extra: d];
      [_server setDebug: _debug];
      [_server setDelegate: self];
      [_server start: d];
    }
  if (isOwnAuxServer)
    {
      _auxServer = [[TestWebServer alloc] initWithAddress: address
						     port: auxPort
						     mode: isDetached
						    extra: d];
      [_auxServer setDebug: _debug];
      [_auxServer setDelegate: self];
      [_auxServer start: d];
    }
}

- (void)_makeReferenceFlags:(id)extra
{
  // trying to guess the execution path
  // and to infer the reference flag set
  if (nil != _request)
    {
      // default reference set
      [self setReferenceFlags: SENTREQUEST];
      [self setReferenceFlags: NOTAUTHORIZED];
      [self setReferenceFlags: AUTHORIZED];
      [self setReferenceFlags: GOTUNAUTHORIZED];
      [self setReferenceFlags: GOTREQUEST];
      [self setReferenceFlags: SENTRESPONSE];
      [self setReferenceFlags: GOTRESPONSE];
      if (nil != _expectedContent)
	{
	  [self setReferenceFlags: GOTCONTENT];
	}
      [self setReferenceFlags: GOTFINISH];
    }
  else
    {
      [NSException raise: NSInvalidArgumentException
		  format: @"no request"];
    }

  if ([extra isKindOfClass: [NSDictionary class]])
    {
      // make correction in the reference flag set
      NSDictionary *d = extra;
      if ((d = [d objectForKey: @"ReferenceFlags"]) != nil)
	{
	  NSEnumerator *en = [d keyEnumerator];
	  NSString *key;
	  NSString *value;
	  SEL sel;

	  while ((key = [en nextObject]) != nil)
	    {
	      value = [d objectForKey: key];
	      NSString *originalKey = nil;
	      NSUInteger flag = NORESULTS;
	      // NSUInteger flag = (NSUInteger)NSMapGet(_flagMap, key);
	      if (NSMapMember(_flagMap,
                key, (void **)&originalKey, (void **)&flag))
		{
		  if ([value isEqualToString: @"YES"])
		    {
		      sel = @selector(setReferenceFlags:);
		    }
		  else
		    {
		      sel = @selector(unsetReferenceFlags:);
		    }

		  [self performSelector: sel withObject: (void *)flag];
		}
	      else
		{
		  [NSException raise: NSInternalInconsistencyException
			      format: @"flag codes corrupted (key %@)", key];
		}

	    }
	}
    }
}

@end /* NSURLConnectionTest (Private) */
