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

# Tests basic compilation and events.

use strict;

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

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

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

use Test::More tests => 41;
use POE;

diag("This test generates some STDERR during trace testing.");

### Test parameters and results.

my $machine_count  = 10;
my $event_count    = 5;
my $sigalrm_caught = 0;
my $sigpipe_caught = 0;
my $sender_count   = 0;
my $got_heap_count = 0;
my $default_count  = 0;

die "machine count must be even" if $machine_count & 1;

### Status registers for each state machine instance.

my ( @completions, @objpack );

#------------------------------------------------------------------------------
# Define a simple state machine.

sub task_start {
  my ($kernel, $session, $heap, $id) = @_[KERNEL, SESSION, HEAP, ARG0];
  $heap->{count} = 0;
  $kernel->yield( count => $id );
}

sub task_run {
  my ($kernel, $session, $heap, $id) = @_[KERNEL, SESSION, HEAP, ARG0];

  $sender_count++ if $_[SENDER] == $session;

  if ($heap->{count} & 1) {
    $kernel->yield( bogus => $id ); # _default
  }
  else {
    $kernel->post( $session, bogus => $id ); # _default
  }

  if ( $kernel->call( $session, next_count => $id ) < $event_count ) {

    if ($heap->{count} & 1) {
      $kernel->yield( count => $id );
    }
    else {
      $kernel->post( $session, count => $id );
    }

  }
  else {
    $heap->{id} = $id;
  }
}

sub task_default {
  return 0 if $_[ARG0] eq '_signal'; # ignore signals
  $default_count++ if $_[STATE] eq '_default';
}

sub task_next_count {
  my ($kernel, $session, $heap, $id) = @_[KERNEL, SESSION, HEAP, ARG0];
  ++$heap->{count};
}

sub task_stop {
  $completions[$_[HEAP]->{id}] = $_[HEAP]->{count};
  $got_heap_count++ if (
    defined($_[HEAP]->{got_heap}) and
    $_[HEAP]->{got_heap} == $_[HEAP]->{id}
  );
}

#------------------------------------------------------------------------------
# Test simple signals.

# Spawn a quick state machine to test signals.  This is a classic
# example of inline states being just that: inline anonymous coderefs.
# It makes quick hacks quicker!
POE::Session->create(
  inline_states => {
    _start => sub {
      $_[HEAP]->{kills_to_go} = $event_count;
      $_[KERNEL]->sig( ALRM => 'sigalrm_target' );
      $_[KERNEL]->sig( PIPE => 'sigpipe_target' );
      $_[KERNEL]->delay( fire_signals => 0.5 );
    },
    fire_signals => sub {
      if ($_[HEAP]->{kills_to_go}--) {
        $_[KERNEL]->delay( fire_signals => 0.5 );
        if ($^O eq 'MSWin32') {
          $_[KERNEL]->signal( $_[KERNEL], 'ALRM' );
          $_[KERNEL]->signal( $_[KERNEL], 'PIPE' );
        }
        else {
          kill ALRM => $$;
          kill PIPE => $$;
        }
      }
      # One last timer so the session lingers long enough to catch
      # the final signal.
      else {
        $_[KERNEL]->delay( done_waiting => 1 );
      }
    },
    sigalrm_target => sub {
      $sigalrm_caught++ if $_[ARG0] eq 'ALRM';
      $_[KERNEL]->sig_handled();
    },
    sigpipe_target => sub {
      $sigpipe_caught++ if $_[ARG0] eq 'PIPE';
      $_[KERNEL]->sig_handled();
    },
    done_waiting => sub {
      $_[KERNEL]->sig( ALRM => undef );
      $_[KERNEL]->sig( PIPE => undef );
    },
    _stop => sub { }, # Pacify assertions.
  }
);

# Spawn ten state machines.
for (my $i=0; $i<$machine_count; $i++) {

  POE::Session->create(
    inline_states => {
      _start     => \&task_start,
      _stop      => \&task_stop,
      count      => \&task_run,
      next_count => \&task_next_count,
      _default   => \&task_default,
    },
    args => [ $i ],
    heap => { got_heap => $i },
  );
}

#------------------------------------------------------------------------------
# Simple client/server sessions using events as inter-session
# communications.  Tests postbacks, too.

POE::Session->create(
  inline_states => {
    _start => sub {
      $_[KERNEL]->alias_set( 'server' );
      $_[HEAP]->{response} = 0;
    },
    sync_query => sub {
      $_[ARG0]->( ++$_[HEAP]->{response} );
    },
    query => sub {
      $_[ARG0]->( ++$_[HEAP]->{response} );
    },
    _stop => sub { }, # Pacify assertions.
  },
);

# A simple client session.  It requests five counts and then stops.
# Its magic is that it passes a postback for the response.

my $postback_test = 1;
my $callback_test = 1;

POE::Session->create(
  inline_states => {
    _start => sub {
      $_[KERNEL]->yield( 'query' );
      $_[HEAP]->{cookie} = 0;
    },
    query => sub {
      $_[KERNEL]->post(
        server =>
        query  => $_[SESSION]->postback(response => ++$_[HEAP]->{cookie})
      );
      $_[HEAP]->{sync_called_back} = 0;
      $_[KERNEL]->call(
        server     =>
        sync_query =>
        $_[SESSION]->callback(sync_response => ++$_[HEAP]->{cookie})
      );
      $callback_test = 0 unless $_[HEAP]->{sync_called_back};
    },
    sync_response => sub {
      my ($req, $rsp) = ($_[ARG0]->[0], $_[ARG1]->[0] + 1);
      $callback_test = 0 unless $req == $rsp;
      $_[HEAP]->{sync_called_back} = 1;
    },
    response => sub {
      my ($req, $rsp) = ($_[ARG0]->[0], $_[ARG1]->[0] - 1);
      $postback_test = 0 unless $req == $rsp;
      if ($_[HEAP]->{cookie} < 5) {
        $_[KERNEL]->yield( 'query' );
      }
    },
    _stop => sub {
      is(
        $_[KERNEL]->get_active_session(), $_[SESSION],
        "get_active_session within session"
      );
      is(
        $_[KERNEL]->get_active_session()->get_heap(), $_[HEAP],
        "get_heap during stop"
      );
    },
  }
);

#------------------------------------------------------------------------------
# Unmapped package session.

package
UnmappedPackage;
use POE::Session; # for constants

sub _start {
  $_[KERNEL]->yield( 'count' );
  $_[HEAP]->{count} = 0;
  $_[HEAP]->{id} = $_[ARG0];
}

sub count {
  return unless $_[OBJECT] eq __PACKAGE__;
  $_[KERNEL]->yield( 'count' ) if ++$_[HEAP]->{count} < $event_count;
}

sub _stop {
  $objpack[$_[HEAP]->{id}] = $_[HEAP]->{count};
}

#------------------------------------------------------------------------------
# Unmapped object session.

package
UnmappedObject;
use POE::Session; # for constants

# Trivial constructor.
sub new { bless [ ], shift; }

sub _start {
  $_[KERNEL]->yield( 'count' );
  $_[HEAP]->{count} = 0;
  $_[HEAP]->{id} = $_[ARG0];
}

sub count {
  return unless ref($_[OBJECT]) eq __PACKAGE__;
  $_[KERNEL]->yield( 'count' ) if ++$_[HEAP]->{count} < $event_count;
}

sub _stop {
  $objpack[$_[HEAP]->{id}] = $_[HEAP]->{count};
}

#------------------------------------------------------------------------------
# Unmapped package session.

package
MappedPackage;
use POE::Session; # for constants

sub my_start {
  $_[KERNEL]->yield( 'count' );
  $_[HEAP]->{count} = 0;
  $_[HEAP]->{id} = $_[ARG0];
}

sub my_count {
  return unless $_[OBJECT] eq __PACKAGE__;
  $_[KERNEL]->yield( 'count' ) if ++$_[HEAP]->{count} < $event_count;
}

sub my_stop {
  $objpack[$_[HEAP]->{id}] = $_[HEAP]->{count};
}

#------------------------------------------------------------------------------
# Unmapped object session.

package
MappedObject;
use POE::Session; # for constants

# Trivial constructor.
sub new { bless [ ], shift; }

sub my_start {
  $_[KERNEL]->yield( 'count' );
  $_[HEAP]->{count} = 0;
  $_[HEAP]->{id} = $_[ARG0];
}

sub my_count {
  return unless ref($_[OBJECT]) eq __PACKAGE__;
  $_[KERNEL]->yield( 'count' ) if ++$_[HEAP]->{count} < $event_count;
}

sub my_stop {
  $objpack[$_[HEAP]->{id}] = $_[HEAP]->{count};
}

#------------------------------------------------------------------------------
# Test the Package and Object sessions.

package
main;

# New style (create) object session without event to method name map.
POE::Session->create(
  object_states => [
    UnmappedObject->new() => [ '_start', 'count', '_stop' ],
  ],
  args => [ 0 ],
);

# New style (create) object session with event to method name map.
POE::Session->create(
  object_states => [
    MappedObject->new => {
      _start => 'my_start',
      count  => 'my_count',
      _stop  => 'my_stop',
    },
  ],
  args => [ 1 ],
);

# New style (create) package session without event to method name map.
POE::Session->create(
  package_states => [
    UnmappedPackage => [ '_start', 'count', '_stop' ],
  ],
  args => [ 2 ],
);

# New style (create) package session with event to method name map.
POE::Session->create(
  package_states => [
    MappedPackage => {
      _start => 'my_start',
      count  => 'my_count',
      _stop  => 'my_stop',
    },
  ],
  args => [ 3 ],
);

#------------------------------------------------------------------------------
# Test changing options
POE::Session->create(
  inline_states => {
    _start => sub {
      my $orig = $_[SESSION]->option(default => 1);
      Test::More::ok($orig, "option original value");
      my $rv = $_[SESSION]->option('default');
      Test::More::ok($rv, "set default option successfully");
      $rv = $_[SESSION]->option('default' => $orig);
      Test::More::ok($rv, "reset default option successfully");
      $rv = $_[SESSION]->option('default');
      Test::More::ok(!($rv xor $orig), "reset default option successfully");

      $_[KERNEL]->yield("idle");
    },
    idle => sub { },
    _stop => sub { }, # Pacify assertions.
  },
  options => { default => 1 },
);

#------------------------------------------------------------------------------
# Test deprecation of new(), test invalid arguments to create()
eval { POE::Session->new("foo" => sub { } ) };
ok($@ ne '', "new() is deprecated");

eval { POE::Session->create("an", "odd", "number", "of", "elephants") };
ok($@ ne '', "create() doesn't accept an odd number of args");

#------------------------------------------------------------------------------
# Main loop.

is(
  $poe_kernel->get_active_session(), $poe_kernel,
  "get_active_session before POE::Kernel->run()"
);

POE::Kernel->run();

is(
  $poe_kernel->get_active_session(), $poe_kernel,
  "get_active_session after POE::Kernel->run()"
);

#------------------------------------------------------------------------------
# Final tests.

# Now make sure they've run.
for (my $i=0; $i<$machine_count; $i++) {
  is(
    $completions[$i], $event_count,
    "test $i ran"
  );
}

# Were all the signals caught?
SKIP: {
  if (($^O eq "MSWin32" or $^O eq "MacOS") and not $ENV{POE_DANTIC}) {
    skip "$^O does not support signals", 2;
  }

  is(
    $sigalrm_caught, $event_count,
    "caught enough SIGALRMs"
  );

  is(
    $sigpipe_caught, $event_count,
    "caught enough SIGPIPEs"
  );
}

# Did the postbacks work?
ok( $postback_test, "postback test" );
ok( $callback_test, "callback test" );

# Gratuitous tests to appease the coverage gods.
ok(
  (ARG1 == ARG0+1) && (ARG2 == ARG1+1) && (ARG3 == ARG2+1) &&
  (ARG4 == ARG3+1) && (ARG5 == ARG4+1) && (ARG6 == ARG5+1) &&
  (ARG7 == ARG6+1) && (ARG8 == ARG7+1) && (ARG9 == ARG8+1),
  "ARG constants are good"
);

is(
  $sender_count, $machine_count * $event_count,
  "sender_count"
);

is(
  $default_count, $machine_count * $event_count,
  "default_count"
);

is(
  $got_heap_count, $machine_count,
  "got_heap_count"
);

# Object/package sessions.
is_deeply(
  \@objpack, [ ($event_count) x 4 ],
  "object/package session event count"
);

my $sessions_destroyed = 0;
my $objects_destroyed = 0;
my $stop_called = 0;
my $parent_called = 0;
my $child_called = 0;

package
POE::MySession;

use vars qw(@ISA);

use POE::Session;
@ISA = qw(POE::Session);

sub DESTROY {
  $_[0]->SUPER::DESTROY;
  $sessions_destroyed++;
}

package
MyObject;

sub new { bless {} }
sub DESTROY { $objects_destroyed++ }

package
main;

POE::MySession->create(
  inline_states => {
    _start => sub {
      $_[HEAP]->{object} = MyObject->new;
      POE::MySession->create(
        inline_states => {
          _start => sub {
            $_[HEAP]->{object} = MyObject->new;
            POE::MySession->create(
              inline_states => {
                _start => sub {
                  $_[HEAP]->{object} = MyObject->new;
                  POE::MySession->create(
                    inline_states => {
                      _start => sub {
                        $_[HEAP]->{object} = MyObject->new;
                        $_[KERNEL]->delay(nonexistent => 3600);
                        $_[KERNEL]->alias_set('test4');
                      },
                      _parent => sub {
                        $parent_called++;
                      },
                      _child => sub { }, # To shush ASSERT
                      _stop => sub {
                        $stop_called++;
                      },
                    },
                  );
                  $_[KERNEL]->delay(nonexistent => 3600);
                  $_[KERNEL]->alias_set('test3');
                },
                _parent => sub {
                  $parent_called++;
                },
                _child => sub {
                  $child_called++ if $_[ARG0] eq 'lose';
                },
                _stop => sub {
                  $stop_called++;
                },
              },
            );
            $_[KERNEL]->delay(nonexistent => 3600);
            $_[KERNEL]->alias_set('test2');
          },
          _parent => sub {
            $parent_called++;
          },
          _child => sub {
            $child_called++ if $_[ARG0] eq 'lose';
          },
          _stop => sub {
            $stop_called++;
          },
        },
      );
      $_[KERNEL]->delay(nonexistent => 3600);
      $_[KERNEL]->alias_set('test1');
      $_[KERNEL]->yield("stop");
    },
    _parent => sub {
      $parent_called++;
    },
    _child => sub {
      $child_called++ if $_[ARG0] eq 'lose';
    },
    _stop => sub {
      $stop_called++;
    },
    stop => sub {
      POE::Kernel->stop();

      my $expected;
      if ($] >= 5.004 and $] < 5.00405) {
        diag( "Note: We find your choice of Perl versions disturbing" );
        diag( "primarily due to the number of bugs POE triggers within" );
        diag( "it.  You should seriously consider upgrading." );
        $expected = 0;
      }
      else {
        $expected = 3;
      }

      is(
        $sessions_destroyed, $expected,
        "$sessions_destroyed sessions destroyed (expected $expected)"
      );

      # 5.004 and 5.005 have some nasty gc issues. Near as I can tell,
      # data inside the heap is surviving the session DESTROY. This
      # isn't possible in a sane and normal world. So if this is giving
      # you fits, consider it a sign that your "legacy perl" fetish is
      # bizarre and harmful.
      if ($] >= 5.006 or ($] >= 5.004 and $] < 5.00405)) {
        $expected = 3;
      } else {
        $expected = 2;
        diag("Detected a memory leak in Perl version $].");
        diag("Please consider upgrading if you use Perl in production.");
      }

      is(
        $objects_destroyed, $expected,
        "$objects_destroyed objects destroyed (expected $expected)"
      );
    }
  }
);

POE::Kernel->run();

is(
  $stop_called, 4,
  "_stop was called the correct number of times"
);

is(
  $child_called, 3,
  "_child wasn't called"
);

is(
  $parent_called, 0,
  "_parent wasn't called"
);

my $expected;
if ($] >= 5.004 and $] < 5.00405) {
  diag( "Seriously.  We've had to create special cases just to cater" );
  diag( "to your freakish 'legacy buggy perl' fetish.  Consider upgrading" );
  $expected = 0;
}
else {
  $expected = 4;
}

is(
  $sessions_destroyed, $expected,
  "destroyed $sessions_destroyed sessions (expected $expected)"
);

# 5.004 and 5.005 have some nasty gc issues. Near as I can tell,
# data inside the heap is surviving the session DESTROY. This
# isn't possible in a sane and normal world.
if($] >= '5.006') {
  $expected = 4;
}
elsif ($] == 5.005_04 or $] == 5.004_05) {
  $expected = 3;
  diag( "Here's yet another special test case to work around memory" );
  diag( "leaks in Perl $]." );
}
else {
  $expected = 4;
}

is(
  $objects_destroyed, $expected,
  "destroyed $objects_destroyed objects (expected $expected)"
);

# This simple session just makes sure we can start another Session and
# another Kernel.  If all goes well, it'll dispatch some events and
# exit normally.

# The restart test dumps core when using Tk with Perl 5.8.0 and
# beyond, but only if they're built without threading support.  It
# happens consistently in a pure Tk test case.  It happens
# consistently in POE's "make test" suite.  It doesn't happen at all
# when running the test by hand.
#
# http://rt.cpan.org/Ticket/Display.html?id=8588 is tracking the Tk
# test case.  Wish us luck there.
#
# Meanwhile, these tests will be skipped under Tk if Perl is 5.8.0 or
# beyond, and it's not built for threading.

SKIP: {
#  use Config;
#  skip "Restarting Tk dumps core in single-threaded perl $]", 6 if (
#    $] >= 5.008 and
#    exists $INC{"Tk.pm"} and
#    !$Config{useithreads}
#  );

  POE::Session->create(
    options => { trace => 1, default => 1, debug => 1 },
    inline_states => {
      _start => sub {
        pass("restarted event loop session _start");
        $_[KERNEL]->yield("woot");
        $_[KERNEL]->delay(narf => 1);
      },
      woot => sub {
        pass("restarted event loop session yield()");
      },
      narf => sub {
        pass("restarted event loop session timer delay()");
      },
      _stop => sub {
        pass("restarted event loop session _stop");
      },
    }
  );

  POE::Kernel->run();
  pass("restarted event loop returned normally");
}

1;
