# vim: ts=2 sw=2 expandtab
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 POE;
use POE::Pipe::TwoWay;
use IO::File;
use Tie::Handle;

# Bring in some constants to save us some typing.
sub MODE_RD () { POE::Kernel::MODE_RD }
sub MODE_WR () { POE::Kernel::MODE_WR }
sub MODE_EX () { POE::Kernel::MODE_EX }

sub HS_RUNNING () { POE::Kernel::HS_RUNNING }
sub HS_PAUSED  () { POE::Kernel::HS_PAUSED  }
sub HS_STOPPED () { POE::Kernel::HS_STOPPED }

sub HSS_HANDLE  () { POE::Kernel::HSS_HANDLE  }
sub HSS_SESSION () { POE::Kernel::HSS_SESSION }
sub HSS_STATE   () { POE::Kernel::HSS_STATE   }
sub HSS_ARGS    () { POE::Kernel::HSS_ARGS    }

sub SH_HANDLE    () { POE::Kernel::SH_HANDLE    }
sub SH_REFCOUNT  () { POE::Kernel::SH_REFCOUNT  }
sub SH_MODECOUNT () { POE::Kernel::SH_MODECOUNT }

use Test::More;

unless (-f "run_network_tests") {
  plan skip_all => "Network access (and permission) required to run this test";
}

plan tests => 132;

### Factored out common tests

# 1 subtest
sub verify_handle_structure {
  my ($name, $handle_info) = @_;

  my $expected_handles = {
    $poe_kernel->ID => do {
      my %h;
      for (@$handle_info) {
        my ($fh, $modes) = @$_;

        my $rd = $modes =~ /r/ ? 1 : 0;
        my $wr = $modes =~ /w/ ? 1 : 0;
        my $ex = $modes =~ /x/ ? 1 : 0;
        die "woops: $modes" if $modes =~ /[^rwx]/;

        $h{fileno($fh)} = [
          $fh,                # SH_HANDLE
          $rd + $wr + $ex,    # SH_REFCOUNT
          [                   # SH_MODECOUNT
            $rd,              #   MODE_RD
            $wr,              #   MODE_WR
            $ex,              #   MODE_EX
          ],
        ];
      };
      \%h;
    },
  };

  my %handles = $poe_kernel->_data_handle_handles();
  is_deeply(
    \%handles,
    $expected_handles,
    "$name: session to handles map"
  );
}

# 3 subtests
sub verify_handle_sessions {
  my ($name, $fh, $read_event, $write_event, $exp_event) = @_;

  my $make_expected = sub {
    my ($event) = @_;
    return +{} unless defined $event;
    return +{
      $poe_kernel->ID => {
        fileno($fh) => [
          $fh,           # HSS_HANDLE
          $poe_kernel,   # HSS_SESSION
          $event,        # HSS_STATE
          [ ],           # HSS_ARGS
        ]
      }
    };
  };

  my ($ses_r, $ses_w, $ses_e) =
    $poe_kernel->_data_handle_fno_sessions(fileno($fh));

  is_deeply(
    $ses_r,
    $make_expected->($read_event),
    "$name: fileno read session"
  );
  is_deeply(
    $ses_w,
    $make_expected->($write_event),
    "$name: fileno write session"
  );
  is_deeply(
    $ses_e,
    $make_expected->($exp_event),
    "$name: fileno expedite session"
  );
}

# 7 subtests
sub verify_handle_refcounts {
  my ($name, $fh, $modes) = @_;

  my $expected_rd = $modes =~ /r/ ? 1 : 0;
  my $expected_wr = $modes =~ /w/ ? 1 : 0;
  my $expected_ex = $modes =~ /x/ ? 1 : 0;
  die "woops: $modes" if $modes =~ /[^rwx]/;

  {
    my ($tot, $rd, $wr, $ex) = $poe_kernel->_data_handle_fno_refcounts(
      fileno($fh)
    );
    is(
      $tot,
      $expected_rd + $expected_wr + $expected_ex,
      "$name: fd total refcount"
    );
    is( $rd, $expected_rd, "$name: fd read refcount" );
    is( $wr, $expected_wr, "$name: fd write refcount" );
    is( $ex, $expected_ex, "$name: fd expedite refcount" );
  }
}

# 6 subtests
sub verify_handle_state {
  my ($name, $fh, $rd_str, $wr_str, $ex_str) = @_;
  # string format: 'AR', A - actual, R - requested

  my $parse_str = sub {
    my ($str) = @_;
    return [ map { +{
        's' => HS_STOPPED,
        'p' => HS_PAUSED,
        'r' => HS_RUNNING }->{$_} }
      split //, $str ];
  };

  my $rd = $parse_str->($rd_str);
  my $wr = $parse_str->($wr_str);
  my $ex = $parse_str->($ex_str);

  my ($r_act, $w_act, $e_act) =
    $poe_kernel->_data_handle_fno_states(fileno($fh));

  ok( $r_act == $$rd[0], "$name: read actual state" );
  ok( $w_act == $$wr[0], "$name: write actual state" );
  ok( $e_act == $$ex[0], "$name: expedite actual state" );
}

### Tests

# Get a baseline reference count for the session, to use as
# comparison.
my $base_refcount = $poe_kernel->_data_ses_refcount($poe_kernel->ID);

# We need some file handles to work with.
my ($a_read, $a_write, $b_read, $b_write) = POE::Pipe::TwoWay->new("inet");
ok(defined($a_read), "created a two-way pipe");

# Add a filehandle in read mode.
$poe_kernel->_data_handle_add($a_read, MODE_RD, $poe_kernel, "event-rd", []);

# Verify reference counts.
ok(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 1,
  "first read add: session reference count"
);
verify_handle_refcounts(
  "first read add", $a_read, "r"
);

# Verify the handle's state.
verify_handle_state(
  "first read add", $a_read,
  "rr", "pp", "pp"
);

# Verify the handle's sessions.
verify_handle_sessions(
  "first read add", $a_read, "event-rd", undef, undef
);

# Verify the handle structure.
verify_handle_structure(
  "first read add",
  [ [$a_read => 'r'] ],
);

# Add a second handle in read mode.

$poe_kernel->_data_handle_add($b_read, MODE_RD, $poe_kernel, "event-rd", []);

# Verify reference counts.

ok(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 2,
  "second read add: session reference count"
);

verify_handle_refcounts(
  "second read add", $b_read, "r"
);

# Verify the handle's state.
verify_handle_state(
  "second read add", $b_read,
  "rr", "pp", "pp"
);

# Verify the handle's sessions.
verify_handle_sessions(
  "second read add", $b_read, "event-rd", undef, undef
);

# Verify the handle structure.
verify_handle_structure(
  "second read add",
  [ [$a_read => 'r'], [$b_read => 'r'] ],
);

# Add a third filehandle in write mode.

$poe_kernel->_data_handle_add($a_write, MODE_WR, $poe_kernel, "event-wr", []);

# Verify reference counts.  Total reference count doesn't go up
# because this is a duplicate fileno of a previous one.
# -><- May not be true on all systems!  Argh!
die "woops, we've assumed that write handles have same fileno as read handles"
  unless fileno($a_write) == fileno($a_read);

ok(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 2,
  "third write add: session reference count"
);

verify_handle_refcounts(
  "third write add", $a_write, "rw"
);

# Verify the handle's state.
verify_handle_state(
  "third write add", $a_write,
  "rr", "rr", "pp"
);

# Verify the handle's sessions.
verify_handle_sessions(
  "third write add", $a_write, "event-rd", "event-wr", undef
);

# Verify the handle structure.
verify_handle_structure(
  "third write add",
  [ [$a_read => 'rw'], [$b_read => 'r'] ],
);

# Add a fourth filehandle in exception mode.

$poe_kernel->_data_handle_add($b_write, MODE_EX, $poe_kernel, "event-ex", []);

# Verify reference counts.

ok(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 2,
  "fourth expedite add: session reference count"
);

verify_handle_refcounts(
  "fourth expedite add", $b_write, "rx"
);

# Verify the handle's state.
verify_handle_state(
  "fourth expedite add", $b_write,
  "rr", "pp", "rr"
);

# Verify the handle's sessions.
verify_handle_sessions(
  "fourth expedite add", $b_write, "event-rd", undef, "event-ex"
);

# Verify the handle structure.
verify_handle_structure(
  "third write add",
  [ [$a_read => 'rw'], [$b_read => 'rx'] ],
);

# Test various handles.
ok(
  $poe_kernel->_data_handle_is_good($a_read,  MODE_RD),
  "a_read in read mode"
);
ok(
  $poe_kernel->_data_handle_is_good($a_read,  MODE_WR),
  "a_read in write mode"
);
ok(
  !$poe_kernel->_data_handle_is_good($a_read,  MODE_EX),
  "a_read in expedite mode"
);

ok(
  $poe_kernel->_data_handle_is_good($a_write, MODE_RD),
  "a_write in read mode"
);
ok(
  $poe_kernel->_data_handle_is_good($a_write, MODE_WR),
  "a_write in write mode"
);
ok(
  !$poe_kernel->_data_handle_is_good($a_write, MODE_EX),
  "a_write in expedite mode"
);

ok(
  $poe_kernel->_data_handle_is_good($b_read,  MODE_RD),
  "b_read in read mode"
);
ok(
  !$poe_kernel->_data_handle_is_good($b_read,  MODE_WR),
  "b_read in write mode"
);
ok(
  $poe_kernel->_data_handle_is_good($b_read,  MODE_EX),
  "b_read in expedite mode"
);

ok(
  $poe_kernel->_data_handle_is_good($b_write, MODE_RD),
  "b_write in read mode"
);
ok(
  !$poe_kernel->_data_handle_is_good($b_write, MODE_WR),
  "b_write in write mode"
);
ok(
  $poe_kernel->_data_handle_is_good($b_write, MODE_EX),
  "b_write in expedite mode"
);

# Verify a proper result for an untracked filehandle.
ok(
  !$poe_kernel->_data_handle_is_good(\*STDIN, MODE_RD),
  "untracked handle in read mode"
);
ok(
  !$poe_kernel->_data_handle_is_good(\*STDIN, MODE_WR),
  "untracked handle in write mode"
);
ok(
  !$poe_kernel->_data_handle_is_good(\*STDIN, MODE_EX),
  "untracked handle in expedite mode"
);

# Enqueue events for ready filenos.
$poe_kernel->_data_handle_enqueue_ready(MODE_RD, fileno($a_read));
$poe_kernel->_data_handle_enqueue_ready(MODE_WR, fileno($a_read));

# Events are dispatched right away, so the handles need not be paused.
verify_handle_state(
  "dequeue one", $a_read,
  "rr", "rr", "pp"
);

# Base refcount is not increased, because the event is actually
# dispatched right away.
is(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID), $base_refcount + 2,
  "dequeue one: session reference count"
);

# Pause a handle.  This will prevent it from becoming "running" after
# events are dispatched.
$poe_kernel->_data_handle_pause($a_read, MODE_RD);

verify_handle_state(
  "pause one", $a_read,
  "pp", "rr", "pp"
);

# Dispatch the event, and verify the session's status.  The sleep()
# call is to simulate slow systems, which always dispatch the events
# because they've taken so long to get here.
sleep(1);
$poe_kernel->_data_ev_dispatch_due();

verify_handle_state(
  "dispatch one", $a_read,
  "pp", "rr", "pp"
);

# Resume a handle, and verify its status.  Since there are no
# outstanding events for the handle, change both the requested and
# actual flags.
$poe_kernel->_data_handle_resume($a_read, MODE_RD);

verify_handle_state(
  "resume one", $a_read,
  "rr", "rr", "pp"
);

# Try out some other handle methods.
ok(
  $poe_kernel->_data_handle_count() == 2,
  "number of handles tracked"
);
ok(
  $poe_kernel->_data_handle_count_ses($poe_kernel->ID) == 2,
  "number of sessions tracking"
);
ok(
  $poe_kernel->_data_handle_count_ses("nonexistent") == 0,
  "number of handles tracked by a nonexistent session"
);

# Remove a filehandle and verify the structures.
$poe_kernel->_data_handle_remove($a_read, MODE_RD, $poe_kernel->ID);

# Verify reference counts.
ok(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 2,
  "first remove: session reference count"
);

verify_handle_refcounts(
  "first remove", $a_read, "w"
);

# Verify the handle's state.
verify_handle_state(
  "first remove", $a_read,
  "ss", "rr", "pp"
);

# Verify the handle's sessions.
verify_handle_sessions(
  "first remove", $a_read, undef, "event-wr", undef
);

# Verify the handle structure.
verify_handle_structure(
  "third write add",
  [ [$a_read => 'w'], [$b_read => 'rx'] ],
);

# Remove a filehandle and verify the structures.
$poe_kernel->_data_handle_remove($a_write, MODE_WR, $poe_kernel->ID);

# Verify reference counts.
ok(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 1,
  "second remove: session reference count"
);
ok(
  !$poe_kernel->_data_handle_is_good($a_write, MODE_WR),
  "second remove: handle removed fully"
);

# Remove a nonexistent filehandle and verify the structures.  We just
# make sure the reference count matches the previous one.
$poe_kernel->_data_handle_remove(\*STDIN, MODE_RD, $poe_kernel->ID);
ok(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 1,
  "nonexistent remove: session reference count"
);

# Now test some special cases

# regular file filehandle
{
  my $fh = IO::File->new($0, "r+");
  $poe_kernel->_data_handle_add($fh, MODE_RD, $poe_kernel, "event-rd", []);
  $poe_kernel->_data_handle_add($fh, MODE_WR, $poe_kernel, "event-wr", []);

  verify_handle_refcounts("regular file", $fh, "rw");
  verify_handle_state("regular file", $fh, "rr", "rr", "pp");
  verify_handle_sessions("regular file", $fh, "event-rd", "event-wr", undef);
  verify_handle_structure("regular file",
    [ [$fh => 'rw'], [$b_read => 'rx'] ]);

  # now pause the handle, check it's paused,
  # then add it again, and check that this resumes it
  $poe_kernel->_data_handle_pause($fh, MODE_RD);
  verify_handle_state("regular file - paused", $fh, "pp", "rr", "pp");
  $poe_kernel->_data_handle_add($fh, MODE_RD, $poe_kernel, "event-rd", []);
  verify_handle_state("regular file - resumed", $fh, "rr", "rr", "pp");

  # get a new handle for the same FD, and try to add it
  # --- this should fail
  {
    my $dup_fh = IO::Handle->new_from_fd(fileno($fh), "r");
    eval {
      $poe_kernel->_data_handle_add($dup_fh, MODE_RD, $poe_kernel,
        "event-rd", []);
    };
    TODO: {
      local $TODO = "Rekeyed file watchers on descriptors for iThread safety";
      ok($@ ne '', "failure when adding different handle but same FD");
    };
  }

  $poe_kernel->_data_handle_remove($fh, MODE_RD, $poe_kernel->ID);
  $poe_kernel->_data_handle_remove($fh, MODE_WR, $poe_kernel->ID);

  ok(
    $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 1,
    "regular file: session reference count"
  );
  ok(
    !$poe_kernel->_data_handle_is_good($fh, MODE_WR)
    && !$poe_kernel->_data_handle_is_good($fh, MODE_RD),
    "regular file: handle removed fully"
  );
}

# tied filehandle
SKIP: {
  BEGIN {
    package My::TiedHandle;
    use vars qw(@ISA);
    @ISA = qw( Tie::StdHandle IO::Handle );
  }
  my $fh = IO::Handle->new;
  tie *$fh, 'My::TiedHandle';

  open *$fh, "+<$0" or skip("couldn't open tied handle: $!", 19);

  $poe_kernel->_data_handle_add($fh, MODE_WR, $poe_kernel, "event-wr", []);
  $poe_kernel->_data_handle_add($fh, MODE_EX, $poe_kernel, "event-ex", []);

  verify_handle_refcounts("tied fh", $fh, "wx");
  verify_handle_state("tied fh", $fh, "pp", "rr", "rr");
  verify_handle_sessions("tied fh", $fh, undef, "event-wr", "event-ex");
  verify_handle_structure("tied fh",
    [ [$fh => 'wx'], [$b_read => 'rx'] ]);

  $poe_kernel->_data_handle_remove($fh, MODE_WR, $poe_kernel->ID);
  $poe_kernel->_data_handle_remove($fh, MODE_EX, $poe_kernel->ID);

  ok(
    $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount + 1,
    "tied fh: session reference count"
  );
  ok(
    !$poe_kernel->_data_handle_is_good($fh, MODE_WR)
    && !$poe_kernel->_data_handle_is_good($fh, MODE_EX),
    "tied fh: handle removed fully"
  );
}

{
  # Enqueue an event for a handle that we're about to remove
  $poe_kernel->_data_handle_enqueue_ready(MODE_RD, fileno($b_write));
  my @verify = ( [ $b_read => 'rx' ] );

  # Add back a write handle.  Can't select on non-sockets on
  # MSWin32, so we skip this check on that platform.
  if ($^O ne "MSWin32") {
    $poe_kernel->_data_handle_add(
      \*STDOUT, MODE_WR, $poe_kernel, "event-wr", []
    );
    push @verify, [ \*STDOUT => 'w' ];
  }

  verify_handle_structure("before final remove all", \@verify);
}

# Remove all handles for the session.  And verify the structures.
$poe_kernel->_data_handle_clear_session($poe_kernel->ID);
ok(
  !$poe_kernel->_data_handle_is_good($b_write, MODE_EX),
  "final remove all: session reference count"
);

# Check again that all handles are gone
ok(
  $poe_kernel->_data_ses_refcount($poe_kernel->ID) == $base_refcount,
  "session reference count is back to base count"
);

# Make sure everything shuts down cleanly.
ok(
  $poe_kernel->_data_handle_finalize(),
  "filehandle subsystem finalization"
);

1;
