The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl -w
use strict;
# use threads; # TODO use threads instead of fork?
# disabled - causes an error in subtest... Why? Bug in Test::More, or am I doing something wrong?

my $min_tpm=1.02;
eval{use Test::More $min_tpm}; # possible that a earlier version would work
if($@){
  plan skip_all => "Test::More $min_tpm not installed";
}else{
  plan tests => 8;
}

use NET::MitM;
use Carp;

print "="x72,"\n";

# echo server - used in most of our tests
sub echoback($){
  return $_[0];
}
# TODO parameterise port number when test run, or at install time, or something
my $next_port=8000+int(rand(1000));
my $echo_port=$next_port++;

sub pause($)
{
  select(undef,undef,undef,shift);
}

# TODO fail if everything isn't done in - say - 60 seconds.  Kinder than locking up. Can be done using alarm() but makes debugging harder. TODO Maybe put a timeout in mainloop?

sub done()
{
  printf("Process %u has been signalled (@_) - exiting.\n",$$);
  exit();
}
sub abort()
{
  confess sprintf("Process %u has been signalled (@_) - exiting.\n",$$);
  BAIL_OUT("signalled");
  exit();
}
$SIG{TERM}=\&done;
$SIG{ALRM}=\&abort;
$SIG{INT}=\&abort;
$SIG{CLD} = "IGNORE";

sub spawn(&)
{
  my $block=shift;
  my $pid=fork();
  #printf "alarm reset %u/%d\n",$$,alarm(10);
  if(!defined $pid){
    #error
    BAIL_OUT("cannot fork: $!");
  }elsif($pid==0){
    #child
    printf "child %u spawned...\n",$$;
    $block->();
    BAIL_OUT("child unexpectedly exited: $!");
  }else{
    #parent
    pause(.1); # should only need a fraction of a second, probably less
    return $pid;
  }
}

sub is_true($){
  return shift(@_) ? 1 : 0;
}

sub is_false($){
  return shift(@_) ? 0 : 1;
}

subtest "echo server creation" => sub {
  my $echo_server = NET::MitM->new_server($echo_port,\&echoback) || BAIL_OUT("failed to start test server: $!");
  $echo_server->name("echo-server");
  is($echo_server->name(),"echo-server","round trip name()");
  is($echo_server->{local_port_num},$echo_port,"initial setting of local port num"); # Note - encapsulation bypassed
  is($echo_server->{server_callback},\&echoback,"initial setting of callback"); # bypasses encapsulation
};

sub new_echo_server($) {
  my $parallel=shift;
  my $echo_server = NET::MitM->new_server($echo_port,\&echoback) || BAIL_OUT("failed to start test server: $!");
  $echo_server->name("echo-server");
  $echo_server->log_file("echo.log");
  #$echo_server->verbose(2);
  if($parallel){
    # cannot test here because this is executed in a different process
    #ok(is_true($echo_server->parallel()),"parallel defaults to true - for now");
  }else{
    #subtest "set parallel/serial" => sub 
    {
      #ok(is_false($echo_server->serial()),"serial defaults to false - for now");
      #ok(is_true($echo_server->parallel()),"parallel defaults to true - for now");
      $echo_server->serial(1);
      #ok(is_true($echo_server->serial()),"setting serial sets serial");
      #ok(is_false($echo_server->parallel()),"setting serial also sets parallel");
    };
  }
  # side-test of _new_child
  my $new_child=$echo_server->_new_child(); # _new_child will complain (fail) if there are attributes it doesn't expect
  $new_child->name(sprintf "echo-server-child-%u",$$);
  print "go...\n";
  $echo_server->go();
  BAIL_OUT("server->go() should not have returned");
  return "this should not have happened";
}

my $echo_server_pid=spawn(sub{new_echo_server(1)});

# It would be nice to be able to run with old or new Test::More.  It is possible to test for the presence of subtest, for eg "if(defined &Test::More::subtest)..." But there is no obvious mechanism for telling the old Test::More how many tests to expect without confusing the new Test::More. 
subtest 'client <-> server (without MitM)' => sub {
  my $test="direct to server";
  my @clients;
  my $repeats=10; # works for up to just over 1000 on linux, the limit on windows is rather lower
  for (1..$repeats){
    $clients[$_] = my $client = NET::MitM->new_client("localhost",$echo_port);
    $client->name("$test-$_");
    my $response = $client->send_and_receive("1234.$_");
    is($response, "1234.$_", "$test: send and receive a string ($_ of $repeats)");
  }
  for (1..$repeats){
    $clients[$_]->disconnect_from_server();
  }
};

subtest 'MitM with no callbacks' => sub {
  my $port2=$next_port++;
  my $MitM_pid = spawn(
    sub{
      my $mitm = NET::MitM->new('localhost',$echo_port,$port2) || BAIL_OUT("failed to start MitM: $!");
      $mitm->name("MitM-$port2");
      #is($mitm->name(),"MitM-$port2","roundtrip name()");
      $mitm->go();
      BAIL_OUT("MitM->go() should not have returned");
    }
  );
  my $client = NET::MitM->new_client("localhost",$port2);
  $client->name("client-$port2");
  my $response = $client->send_and_receive("232");
  is($response,"232","send and receive a string");
  $client->disconnect_from_server();
  pause(.1); # should only need a fraction of a second
  printf "Signalling MitM: %u\n",$MitM_pid;
  kill 'TERM', $MitM_pid or warn "missed: $!";
};

# note - log1 and log2 are called in the child process, not in the parent - cannot be used to return a value to parent when running in parallel

my @log1=">";
sub log1($)
{
  print "++ log1 called ++\n";
  return shift;
}

my @log2;
sub log2($)
{
  print "++ log2 called ++\n";
  return undef;
}

subtest 'MitM with readonly callbacks'=>sub {
  my $port2=$next_port++;
  my $MitM_pid = spawn(
    sub{
      my $mitm = NET::MitM->new('localhost',$echo_port,$port2) || BAIL_OUT("failed to start MitM: $!");
      $mitm->client_to_server_callback(\&log1);
      $mitm->server_to_client_callback(\&log2);
      $mitm->name("mitm-$port2");
      $mitm->go();
      BAIL_OUT("MitM->go() should not have returned");
    }
  );
  my $client = NET::MitM->new_client("localhost",$port2);
  $client->name("client-$port2");
  my $response = $client->send_and_receive("234");
  is($response,"234","send and receive a string");
  $client->disconnect_from_server();
  pause(.1); # should only need a fraction of a second
  printf "Signalling MitM: %u\n",$MitM_pid;
  kill 'TERM', $MitM_pid or warn "missed: $!";
};

sub manipulate1($)
{
  my $str = shift;
  $str =~ s/a/A/;
  return $str;
}

sub manipulate2($)
{
  my $str = shift;
  $str =~ s/e/E/;
  return $str;
}

sub with_readwrite($){
  my $parallel=shift;
  my $test="MitM with readwrite callbacks - parallel=$parallel";
  my $port2=$next_port++;
  my $MitM_pid = spawn(
    sub{
      my $MitM = NET::MitM->new('localhost',$echo_port,$port2) || BAIL_OUT("failed to start MitM: $!");;
      $MitM->name("mitm-$port2");
      $MitM->server_to_client_callback(\&manipulate1);
      $MitM->client_to_server_callback(\&manipulate2);
      $MitM->parallel(1) if $parallel;
      $MitM->go();
      BAIL_OUT("MitM->go() should not have returned");
    }
  );
  my $client = NET::MitM->new_client("localhost",$port2);
  $client->name("client-$port2");
  my $response = $client->send_and_receive("abc");
  is($response,"Abc","$test: request manipulation");
  $response = $client->send_and_receive("def");
  is($response,"dEf","$test: response manipulation");
  $client->disconnect_from_server();
  pause(.1); # should only need a fraction of a second
  printf "Signalling MitM: %u\n",$MitM_pid;
  kill 'TERM', $MitM_pid or warn "missed: $!";
}

subtest "MitM with readwrite callbacks - serial" => sub {
  with_readwrite(0);
};

subtest "MitM with readwrite callbacks - parallel" => sub {
  with_readwrite(1);
};

{
  my $done=0;

  subtest 'MitM with timer_callback'=>sub {
    my $port2=$next_port++;
    sub do_once(){
      return 0;
    }
    my $mitm = NET::MitM->new('localhost',$echo_port,$port2) || BAIL_OUT("failed to start MitM: $!");
    $mitm->timer_callback(0,\&do_once);
    my ($interval,$callback) = $mitm->timer_callback();
    is($interval,1); # sanity check - mitm disallows an interval of exactly 0
    $mitm->timer_callback(2,\&do_once);
    $mitm->name("mitm-$port2");
    ($interval,$callback) = $mitm->timer_callback();
    is($interval,2);
    is($callback,\&do_once);
    my $t1 = time();
    alarm 5; # if something goes wrong, don't silently hang forever - user may still need to ^C, but at least tell them
    $mitm->go();
    alarm 0;
    my $t2 = time();
    my $t_diff=$t2-$t1;
    # without Time::HiRes, is only accurate to the second, and potentially not even that accurate
    ok($t_diff >= 1, "go() took $t_diff seconds, should take at least 1 second (hopefully, 2)");
    ok($t_diff <= 3, "go() took $t_diff seconds, should take no more than 3 seconds (hopefully, 2)");
    sub do_till_done(){
      return !$done;
    }
    sub set_done(){
      $done=1;
      return shift;
    }
    $mitm->timer_callback(2,\&do_till_done);
    $mitm->server_to_client_callback(\&set_done);
    $mitm->listen();
    my $client = NET::MitM->new_client("localhost",$port2);
    $client->name("client-$port2");
    $client->send_to_server("ping");
    $mitm->timer_callback(.1,\&do_till_done);
    is(scalar(@{$mitm->{children}}),0,"no children yet"); # note - breaks encapsulation
    $mitm->go();
    is(scalar(@{$mitm->{children}}),1,"one child now"); # note - breaks encapsulation
    is($done,1,"do till done");
    my $resp = $client->read_from_server("ping");
    is($resp,"ping","round trip");
    $client->_destroy(); # TODO provide a user callable method? Would need to clean up children - if running in serial
    $mitm->go();
    is(scalar(@{$mitm->{children}}),0,"closing client should terminate children"); # note - breaks encapsulation
    $mitm->_destroy();
    # TODO how to test that all ports have been properly closed? lsof? alloc a file handle and check it has value #4?
  };
}

SKIP: {
  eval { use Time::HiRes qw(time sleep)};
  if($@){
    skip "Time::HiRes, which is not installed, is required for sub-second accuracy of timer_interval. You may still specify fractions of a second, MitM will out by up to a second each time, but it will average out. If this is not precise enough, please install Time::HiRes.\n";
  }
  my $to_go=10;
  subtest 'timer_interval precision'=>sub {
    my $port2=$next_port++;
    my $mitm = NET::MitM->new('localhost',$echo_port,$port2) || BAIL_OUT("failed to start MitM: $!");
    $mitm->verbose(2);
    sub do_til_done(){
      print "done=$to_go\n";
      if(--$to_go>0){
	sleep 0.1;
	return 1;
      }else{
	return 0;
      }
    }
    $mitm->timer_callback(.2,\&do_til_done);
    $mitm->name("mitm-$port2");
    my ($interval,$callback) = $mitm->timer_callback();
    is($interval,.2);
    is($callback,\&do_til_done);
    my $t1 = time();
    $mitm->go();
    my $t2 = time();
    my $t_diff=$t2-$t1;
    # on my boxes, takes ~2.00125 seconds on windows, ~2.00098 on linx. Allow +/- 0.1. It averages out over a long run.
    ok($t_diff >= 1.9 && $t_diff <= 2.1, "go() took $t_diff seconds, should take close to 2 seconds");
  };
}

if(0) # TODO Automated test not working yet - manual testing suggests code works fine
{
  my $test="defrag_delay";
  my $port2=$next_port++;
  sub mark_fragments($){return qq{[$_[0]]}};
  my $MitM_pid = spawn(
    sub{
      my $MitM = NET::MitM->new('localhost',$echo_port,$port2);
      $MitM->name("mitm-$port2");
      $MitM->server_to_client_callback(\&mark_fragments);
      $MitM->client_to_server_callback(\&mark_fragments);
      $MitM->defrag_delay(0);
      $MitM->log_file("defrag.log");
      $MitM->go();
    }
  );
  my $client = NET::MitM->new_client("localhost",$port2);
  $client->name("client-$port2");
  my $delay=0.1; # guess
  for("a".."j"){ # FIXME - only works up to about 10 message, or maybe that is all that makes sense?
    pause($delay);
    $client->sendToServer($_);
  }
  my $response1 = $client->read_from_server(); 
  isnt($response1,"[[abcdefghij]]","$test: test the test case - ensure some fragmentation is occurring so we can prove our 'prevention' is doing something");
  $client->disconnect_from_server();
  pause(.1);
  printf "Signalling MitM: %u\n",$MitM_pid;
  kill 'TERM', $MitM_pid or warn "missed: $!";
  $port2=$next_port++;
  $MitM_pid = spawn(
    sub{
      my $MitM = NET::MitM->new('localhost',$echo_port,$port2);
      $MitM->name("mitm-$port2");
      $MitM->server_to_client_callback(\&mark_fragments);
      $MitM->client_to_server_callback(\&mark_fragments);
      $MitM->defrag_delay(3);
      $MitM->log_file("defrag2.log");
      $MitM->verbose(1);
      $MitM->go();
    }
  );
  $client = NET::MitM->new_client("localhost",$port2);
  $client->name("client-$port2");
  for("a".."j"){
    pause($delay);
    $client->sendToServer($_);
  }
  my $response2 = $client->read_from_server(); # TODO add a timeout :-(
  is($response2,"[[abcdefghij]]","$test: our messages, sent so close together, should have been defragmented into a single message");
  $client->disconnect_from_server();
  pause(.1); # should only need a fraction of a second
  printf "Signalling MitM: %u\n",$MitM_pid;
  kill 'TERM', $MitM_pid or warn "missed: $!";
}

pause(.1); # let children exit - they should already have been signalled
printf "Signalling echo server: %u\n",$echo_server_pid;
print kill('TERM',$echo_server_pid) or warn "Failed to kill echo server\n";
pause(.1); # let echo server die
done_testing(); # not supported by old versions of Test::More

print "="x72,"\n";