#!/usr/pkg/bin/perl -w
# vim: ts=2 sw=2 expandtab

# Exercises Wheel::FollowTail, Wheel::ReadWrite, and Filter::Block.
# TODO - Needs tests for Seek and SeekBack.

use strict;
use lib qw(./mylib ../mylib);
use Socket;

sub POE::Kernel::ASSERT_DEFAULT () { 1 }
sub POE::Kernel::CATCH_EXCEPTIONS () { 0 }

BEGIN {
  package
  POE::Kernel;
  use constant TRACE_DEFAULT => exists($INC{'Devel/Cover.pm'});
}

use Test::More;

plan tests => 10;

use POE qw(
  Component::Server::TCP
  Wheel::FollowTail
  Wheel::ReadWrite
  Wheel::SocketFactory
  Filter::Line
  Filter::Block
  Driver::SysRW
);

sub DEBUG () { 0 }

my $tcp_server_port;
my $max_send_count  = 10;    # expected to be even

###############################################################################
# A generic server session.

sub sss_new {
  my ($socket, $peer_addr, $peer_port) = @_;
  POE::Session->create(
    inline_states => {
      _start      => \&sss_start,
      _stop       => \&sss_stop,
      got_error   => \&sss_error,
      got_block   => \&sss_block,
      ev_timeout  => sub {
        DEBUG and warn "=== handle tail got timeout";
        delete $_[HEAP]->{wheel};
      },
    },
    args => [ $socket, $peer_addr, $peer_port ],
  );
}

sub sss_start {
  my ($heap, $socket, $peer_addr, $peer_port) = @_[HEAP, ARG0..ARG2];

  delete $heap->{wheel};
  $heap->{wheel} = POE::Wheel::FollowTail->new(
    Handle       => $socket,
    Driver       => POE::Driver::SysRW->new( BlockSize => 24 ),
    Filter       => POE::Filter::Block->new( BlockSize => 16 ),
    InputEvent   => 'got_block_nonexistent',
    ErrorEvent   => 'got_error_nonexistent',
  );

  # Test event changing.
  $heap->{wheel}->event(
    InputEvent => 'got_block',
    ErrorEvent => 'got_error',
  );

  $heap->{test_two} = 1;
  $heap->{wheel_id} = $heap->{wheel}->ID;
  $heap->{read_count} = 0;
}

sub sss_block {
  my ($kernel, $heap, $block) = @_[KERNEL, HEAP, ARG0];
  DEBUG and warn "=== handle tail got block ($block)";

  if ($block eq 'DONEDONEDONEDONE') {
    $kernel->delay( ev_timeout => 1 );
    return;
  }

  $heap->{read_count}++;
  $kernel->delay( ev_timeout => 10 );
}

sub sss_error {
  my ($heap, $syscall, $errnum, $errstr, $wheel_id) = @_[HEAP, ARG0..ARG3];
  DEBUG and warn "=== handle tail got $syscall error $errnum: $errstr";
  $_[HEAP]->{test_two} = 0 if $errnum;
}

sub sss_stop {
  my $heap = $_[HEAP];
  DEBUG and warn "=== handle tail stopped";
  ok($heap->{test_two}, "handle tail test two");
  is(
    $heap->{read_count}, $max_send_count,
    "handle tail read everything we were sent " .
    "did($heap->{read_count}) wanted($max_send_count)"
  );
}

###############################################################################
# A TCP socket client.

sub client_tcp_start {
  my $heap = $_[HEAP];

  DEBUG and warn "=== client tcp started";

  $heap->{wheel} = POE::Wheel::SocketFactory->new(
    RemoteAddress  => '127.0.0.1',
    RemotePort    => $tcp_server_port,
    SuccessEvent  => 'got_server_nonexistent',
    FailureEvent  => 'got_error_nonexistent',
  );

  # Test event changing.
  $heap->{wheel}->event(
    SuccessEvent => 'got_server',
    FailureEvent => 'got_error',
  );

  $heap->{socketfactory_wheel_id} = $heap->{wheel}->ID;
  $heap->{test_three} = 1;
}

sub client_tcp_stop {
  my $heap =$_[HEAP];
  ok(
    $heap->{test_three},
    "test three"
  );
  ok(
    $heap->{put_count} == $max_send_count,
    "sent everything we should"
  );

  my $sent_count = $_[HEAP]->{put_count} / 2;
  ok(
    $heap->{flush_count} == $sent_count,
    "flushed what we sent (flush=$heap->{flush_count}; sent=$sent_count)"
  );
  ok(
    $heap->{test_six},
    "test six"
  );
}

sub client_tcp_connected {
  my ($kernel, $heap, $server_socket) = @_[KERNEL, HEAP, ARG0];

  delete $heap->{wheel};
  $heap->{wheel} = POE::Wheel::ReadWrite->new(
    Handle       => $server_socket,
    Driver       => POE::Driver::SysRW->new( BlockSize => 32 ),
    Filter       => POE::Filter::Block->new( BlockSize => 16 ),
    ErrorEvent   => 'got_error_nonexistent',
    FlushedEvent => 'got_flush_nonexistent',
  );

  DEBUG and warn "=== client tcp connected";

  # Test event changing.
  $heap->{wheel}->event(
    ErrorEvent   => 'got_error',
    FlushedEvent => 'got_flush',
  );

  $heap->{test_six} = 1;
  $heap->{readwrite_wheel_id} = $heap->{wheel}->ID;

  $heap->{flush_count} = 0;
  $heap->{put_count}   = 0;

  $kernel->yield( 'next_send' );
}

sub client_tcp_next_send {
  my ($kernel, $heap, $line) = @_[KERNEL, HEAP, ARG0];

  DEBUG and warn "=== client tcp got alarm";

  $heap->{wheel}->put( '0123456789ABCDEF0123456789ABCDEF' );
  $heap->{put_count} += 2;

  # Trigger shutdown after the last test send.
  if ($heap->{put_count} >= $max_send_count) {
    $heap->{wheel}->put( 'DONEDONEDONEDONE' );
  }
}

sub client_tcp_got_error {
  my ($heap, $operation, $errnum, $errstr, $wheel_id) = @_[HEAP, ARG0..ARG3];

  if ($wheel_id == $heap->{socketfactory_wheel_id}) {
    $heap->{test_three} = 0;
  }

  if ($wheel_id == $heap->{readwrite_wheel_id}) {
    $heap->{test_six} = 0;
  }

  delete $heap->{wheel};
  warn "$operation error $errnum: $errstr";
}

sub client_tcp_got_flush {
  $_[HEAP]->{flush_count}++;
  DEBUG and warn "=== client_tcp_got_flush";

  if ($_[HEAP]->{put_count} < $max_send_count) {
    # Puts a little delay between puts.
    $_[KERNEL]->delay( next_send => 0.100 );
  }
  else {
    # Delays destruction until all data is out.
    delete $_[HEAP]->{wheel};
  }
}

###############################################################################
# Start the TCP server and client.

SKIP: {
  unless (-f "run_network_tests") {
    skip "network access (and permission) required to run this test", 7;
  }

  POE::Component::Server::TCP->new(
    Port     => 0,
    Address  => '127.0.0.1',
    Acceptor => sub {
      &sss_new(@_[ARG0..ARG2]);
      # This next badness is just for testing.
      my $sockname = $_[HEAP]->{listener}->getsockname();
      delete $_[HEAP]->{listener};

      my ($port, $addr) = sockaddr_in($sockname);
      $addr = inet_ntoa($addr);

      ok(
        ($addr eq '127.0.0.1') && ($port == $tcp_server_port),
        "received connection"
      );
    },
    Started  => sub {
      $tcp_server_port = (
        sockaddr_in($_[HEAP]->{listener}->getsockname())
      )[0];
    },
  );

  POE::Session->create(
    inline_states => {
      _start     => \&client_tcp_start,
      _stop      => \&client_tcp_stop,
      got_server => \&client_tcp_connected,
      got_error  => \&client_tcp_got_error,
      got_flush  => \&client_tcp_got_flush,
      next_send  => \&client_tcp_next_send,
    }
  );
}

### Test a file that appears and disappears.

SKIP: {
  if (($^O eq 'MSWin32' or $^O eq 'cygwin') and not $ENV{POE_DANTIC}) {
    skip "Can't test file reset on $^O because the OS locks opened files", 2;
  }

  POE::Session->create(
    inline_states => {
      _start => sub {
        my ($kernel, $heap) = @_[KERNEL, HEAP];

        unlink "./test-tail-file";
        $heap->{wheel} = POE::Wheel::FollowTail->new(
          Filter        => POE::Filter::Line->new(Literal => "\n"),
          Filename      => "./test-tail-file",
          InputEvent    => "got_input",
          ErrorEvent    => "got_error",
          ResetEvent    => "got_reset",
          IdleEvent     => "create_file",
          PollInterval  => 0.500,
        );
        $heap->{sent_count}  = 0;
        $heap->{recv_count}  = 0;
        $heap->{reset_count} = 0;
        DEBUG and warn "=== file tail start";
      },

      create_file => sub {
        open(FH, ">./test-tail-file") or die $!;
        print FH "moo\n";
        close FH;
        DEBUG and warn "=== file tail create file";
        $_[HEAP]->{sent_count}++;

        # Make file creation a one-shot occurrence.
        $_[HEAP]->{wheel}->event( IdleEvent => undef );
      },

      got_input => sub {
        my ($kernel, $heap) = @_[KERNEL, HEAP];
        $heap->{recv_count}++;

        DEBUG and warn "=== file tail input: $_[ARG0]\n";

        unlink "./test-tail-file";

        if ($heap->{recv_count} == 1) {
          # Trigger a new one-shot file creation.
          $heap->{wheel}->event( IdleEvent => 'create_file' );
          return;
        }

        delete $heap->{wheel};
      },

      got_error => sub { warn "$_[ARG0] error $_[ARG1]: $_[ARG2]"; die },

      got_reset => sub {
        DEBUG and warn "=== file tail got reset";
        $_[HEAP]->{reset_count}++;
      },

      _stop => sub {
        DEBUG and warn "=== file tail stop";
        my $heap = $_[HEAP];
        ok(
          ($heap->{sent_count} == $heap->{recv_count}) &&
          ($heap->{sent_count} == 2),
          "file tail sent and received everything we should " .
          "sent($heap->{sent_count}) recv($heap->{recv_count}) wanted(2)"
        );
        is($heap->{reset_count}, 2, "file tail resets detected");
      },
    },
  );
}

### main loop

POE::Kernel->run();

pass("run() returned successfully");

1;
