#import "UdpSocketTest.h"
#import "UdpSocket.h"
#import "Util.h"
#import "IpAddress.h"
#import <CoreFoundation/CFSocket.h>
#import "TestUtil.h"
#import "ThreadManager.h"
#import "CancelTokenSource.h"

@implementation UdpSocketTest
-(void) testSpecifiedPortLocally {
    CancelTokenSource* receiverLife = [CancelTokenSource cancelTokenSource];
    CancelTokenSource* senderLife = [CancelTokenSource cancelTokenSource];
    
    __block NSData* received = nil;
    __block bool senderReceivedData = false;
    NSData* r1 = [@[@2,@3,@5] toUint8Data];
    NSData* r2 = [@[@7,@11,@13] toUint8Data];
    NSData* r3 = [@[@17,@19,@23] toUint8Data];
    
    in_port_t port1 = (in_port_t)(arc4random_uniform(40000) + 10000);
    in_port_t port2 = port1 + (in_port_t)1;
    
    UdpSocket* receiver = [UdpSocket udpSocketFromLocalPort:port1 toRemoteEndPoint:[IpEndPoint ipEndPointAtAddress:[IpAddress localhost] onPort:port2]];
    UdpSocket* sender = [UdpSocket udpSocketFromLocalPort:port2 toRemoteEndPoint:[IpEndPoint ipEndPointAtAddress:[IpAddress localhost] onPort:port1]];
    [receiver startWithHandler:[PacketHandler packetHandler:^(id packet) {
        received = packet;
    } withErrorHandler:^(id error, id relatedInfo, bool causedTermination) {
        test(false);
    }] untilCancelled:[receiverLife getToken]];
    __block bool failed = false;
    [sender startWithHandler:[PacketHandler packetHandler:^(NSData* packet) {
        // there's a length check here because when the destination is unreachable the sender sometimes gets a superfluous empty data callback... no idea why.
        senderReceivedData |= [packet length] > 0;
    } withErrorHandler:^(id error, id relatedInfo, bool causedTermination) {
        failed = true;
    }] untilCancelled:[senderLife getToken]];
    
    test([receiver isLocalPortKnown]);
    test([receiver localPort] == port1);
    test([sender isLocalPortKnown]);
    test([sender localPort] == port2);

    testChurnAndConditionMustStayTrue(received == nil, 0.1);
    
    [sender send:r1];
    testChurnUntil([received isEqualToData:r1], 1.0);
    test([received isEqualToData:r1]);
    
    [sender send:r2];
    testChurnUntil([received isEqualToData:r2], 1.0);
    
    [receiverLife cancel];
    test(!failed);
    [sender send:r3];
    testChurnUntil(failed, 1.0);
    test([received isEqualToData:r2]);
    
    [senderLife cancel];
    test(!senderReceivedData);
}
-(void) testArbitraryPortLocally {
    CancelTokenSource* receiverLife = [CancelTokenSource cancelTokenSource];
    CancelTokenSource* senderLife = [CancelTokenSource cancelTokenSource];
    
    __block NSData* received = nil;
    __block bool senderReceivedData = false;
    NSData* r1 = [@[@2,@3,@5] toUint8Data];
    NSData* r2 = [@[@7,@11,@13] toUint8Data];
    NSData* r3 = [@[@17,@19,@23] toUint8Data];
    
    in_port_t unusedPort = (in_port_t)(arc4random_uniform(40000) + 10000);
    
    UdpSocket* receiver = [UdpSocket udpSocketTo:[IpEndPoint ipEndPointAtAddress:[IpAddress localhost]
                                                                          onPort:unusedPort]];
    [receiver startWithHandler:[PacketHandler packetHandler:^(id packet) {
        @synchronized (churnLock()) {
            received = packet;
        }
    } withErrorHandler:^(id error, id relatedInfo, bool causedTermination) {
        test(false);
    }] untilCancelled:[receiverLife getToken]];
    
    __block bool failed = false;
    UdpSocket* sender = [UdpSocket udpSocketFromLocalPort:unusedPort
                                         toRemoteEndPoint:[IpEndPoint ipEndPointAtAddress:[IpAddress localhost]
                                                                                   onPort:[receiver localPort]]];
    [sender startWithHandler:[PacketHandler packetHandler:^(NSData* packet) {
        // there's a length check here because when the destination is unreachable the sender sometimes gets a superfluous empty data callback... no idea why.
        senderReceivedData |= [packet length] > 0;
    } withErrorHandler:^(id error, id relatedInfo, bool causedTermination) {
        failed = true;
    }] untilCancelled:[senderLife getToken]];
    
    
    testChurnAndConditionMustStayTrue(received == nil, 0.1);
    
    [sender send:r1];
    testChurnUntil([received isEqualToData:r1], 1.0);
    
    [sender send:r2];
    testChurnUntil([received isEqualToData:r2], 1.0);
    
    [receiverLife cancel];
    test(!failed);
    [sender send:r3];
    testChurnAndConditionMustStayTrue([received isEqualToData:r2], 0.1);
    test([received isEqualToData:r2]);
    
    [senderLife cancel];
    test(!senderReceivedData);
}
-(void) testUdpListen {
    CancelTokenSource* receiverLife = [CancelTokenSource cancelTokenSource];
    CancelTokenSource* senderLife = [CancelTokenSource cancelTokenSource];
    
    __block NSUInteger listenerReceiveCount = 0;
    __block NSUInteger listenerReceiveLength = 0;
    __block NSData* listenerReceivedLast = nil;
    __block NSUInteger clientReceiveCount = 0;
    __block NSUInteger clientReceiveLength = 0;
    __block NSData* clientReceivedLast = nil;
    
    in_port_t port = (in_port_t)(arc4random_uniform(40000) + 10000);
    
    UdpSocket* listener = [UdpSocket udpSocketToFirstSenderOnLocalPort:port];
    [listener startWithHandler:[PacketHandler packetHandler:^(NSData* packet) {
        listenerReceiveCount += 1;
        listenerReceiveLength += [packet length];
        listenerReceivedLast = packet;
    } withErrorHandler:^(id error, id relatedInfo, bool causedTermination) {
        test(false);
    }] untilCancelled:[receiverLife getToken]];
    
    IpEndPoint* e = [IpEndPoint ipEndPointAtAddress:[IpAddress localhost] onPort:port];
    UdpSocket* client = [UdpSocket udpSocketTo:e];
    [client startWithHandler:[PacketHandler packetHandler:^(NSData* packet) {
        clientReceiveCount += 1;
        clientReceiveLength += [packet length];
        clientReceivedLast = packet;
    } withErrorHandler:^(id error, id relatedInfo, bool causedTermination) {
        test(false);
    }] untilCancelled:[senderLife getToken]];
    
    test(![listener isRemoteEndPointKnown]);
    testThrows([listener remoteEndPoint]);
    test([client isRemoteEndPointKnown]);
    test([client remoteEndPoint] == e);
    test(listenerReceiveCount == 0);
    test(clientReceiveCount == 0);
    
    [client send:increasingData(10)];
    testChurnUntil(listenerReceiveCount > 0, 1.0);
    test(clientReceiveCount == 0);
    test([listener isRemoteEndPointKnown]);
    test([[[[listener remoteEndPoint] address] description] isEqualToString:@"127.0.0.1"]);
    test(listenerReceiveLength == 10);
    test([listenerReceivedLast isEqualToData:increasingData(10)]);
    
    [listener send:increasingData(20)];
    testChurnUntil(clientReceiveCount > 0, 1.0);
    test(listenerReceiveCount == 1);
    test(clientReceiveCount == 1);
    test(clientReceiveLength == 20);
    test([clientReceivedLast isEqualToData:increasingData(20)]);
    
    [receiverLife cancel];
    [senderLife cancel];
}
-(void) testUdpFail {
    CancelTokenSource* life = [CancelTokenSource cancelTokenSource];
    
    in_port_t unusedPort = 10000 + (in_port_t)arc4random_uniform(30000);
    UdpSocket* udp = [UdpSocket udpSocketTo:[IpEndPoint ipEndPointAtAddress:[IpAddress localhost] onPort:unusedPort]];
    __block bool failed = false;
    [udp startWithHandler:[PacketHandler packetHandler:^(id packet) {
        test(false);
    } withErrorHandler:^(id error, id relatedInfo, bool causedTermination) {
        failed = true;
    }] untilCancelled:[life getToken]];
    
    [udp send:increasingData(20)];
    testChurnUntil(failed, 1.0);
    
    [life cancel];
}
@end
