The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Async::Selector::Test::Dummy;
use strict;
use warnings;

sub new {
    my ($class) = @_;
    return bless {}, $class;
}

package main;
use strict;
use warnings;
use Test::More;
use Test::Exception;
use Async::Selector;
use Carp;
use Test::Memory::Cycle;

BEGIN {
    use_ok('Async::Selector::Aggregator');
}

sub create_selector {
    my $selector = Async::Selector->new();
    $selector->register(a => sub { undef });
    return $selector;
}

sub create_child {
    my ($selector, $type) = @_;
    if($type eq 'watcher') {
        return $selector->watch(a => 0, sub {});
    }elsif($type eq 'aggregator') {
        my $agg = Async::Selector::Aggregator->new();
        $agg->add(create_child($selector, 'watcher'));
        return $agg;
    }
    croak('This should not happen');
}

sub test_pure_children {
    my ($child_type) = @_;
    note("--- --- child: $child_type");
    {
        my $selector = create_selector();
        my @watchers = map { create_child($selector, $child_type) } 1..3;
        my $agg = new_ok('Async::Selector::Aggregator');
        $agg->add($_) foreach @watchers;
        foreach my $i (0 .. $#watchers) {
            ok($watchers[$i]->active, "watcher $i is active");
        }
        ok($agg->active, 'agg is active');
        is_deeply([$agg->watchers], \@watchers, 'watchers OK');
        is(scalar($selector->watchers), 3, "3 active watchers in selector");
        memory_cycle_ok($agg, "no cyclic ref in agg");
        $agg->cancel();
        foreach my $i (0 .. $#watchers) {
            ok(!$watchers[$i]->active, "watcher $i is inactive");
        }
        ok(!$agg->active, 'agg is inactive');
        is_deeply([$agg->watchers], \@watchers, 'watchers OK');
        is(scalar($selector->watchers), 0, "0 active watcher in selector");

        my $new_watcher = create_child($selector, $child_type);
        ok($new_watcher->active, 'new_watcher is active at first');
        push(@watchers, $new_watcher);
        $agg->add($new_watcher);
        ok(!$new_watcher->active, 'new_watcher becomes inactive because agg is inactive');
        ok(!$agg->active, 'agg is inactive');
        is_deeply([$agg->watchers], \@watchers, 'watchers OK');
        is(scalar($selector->watchers), 0, "0 active watcher in selector");

        $new_watcher = create_child($selector, $child_type);
        $new_watcher->cancel();
        ok(!$new_watcher->active, 'new_watcher is inactive');
        push(@watchers, $new_watcher);
        $agg->add($new_watcher);
        ok(!$new_watcher->active, 'new_watcher is still inactive');
        ok(!$agg->active, 'agg is inactive');
        is_deeply([$agg->watchers], \@watchers, 'watchers OK');
        is(scalar($selector->watchers), 0, "0 active watcher in selector");
    }
    {
        my $agg = new_ok('Async::Selector::Aggregator');
        ok($agg->active, "agg is active at first");
        my $selector = create_selector();
        my @watchers = ();
        my $new_watcher = create_child($selector, $child_type);
        $new_watcher->cancel();
        ok(!$new_watcher->active, 'new_watcher is inactive');
        push(@watchers, $new_watcher);
        $agg->add($new_watcher);
        ok(!$new_watcher->active, 'new_watcher is inactive');
        ok(!$agg->active, 'agg becomes inactive because an inactive watcher is added');
        is_deeply([$agg->watchers], \@watchers, 'watchers OK');
        is(scalar($selector->watchers), 0, '0 active watchers');

        $new_watcher = create_child($selector, $child_type);
        push(@watchers, $new_watcher);
        ok($new_watcher->active, 'new_watcher is active');
        is(scalar($selector->watchers), 1, '1 active watcher');
        $agg->add($new_watcher);
        ok(!$new_watcher->active, 'new_watcher becomes inactive');
        is(scalar($selector->watchers), 0, '0 active watcher');
        ok(!$agg->active, 'agg is inactive');
        is_deeply([$agg->watchers], \@watchers, 'watchers OK');

        $new_watcher = create_child($selector, $child_type);
        push(@watchers, $new_watcher);
        $new_watcher->cancel();
        ok(!$new_watcher->active, 'new_watcher is inactive');
        $agg->add($new_watcher);
        ok(!$new_watcher->active, 'new_watcher is still inactive');
        ok(!$agg->active, 'agg is inactive');
        is_deeply([$agg->watchers], \@watchers, 'watchers OK');
    }
    {
        my $agg = new_ok('Async::Selector::Aggregator');
        my $selector = create_selector();
        my @watchers = map { create_child($selector, $child_type) } 1..3;
        $agg->add($_) foreach @watchers;
        foreach my $i (0 .. $#watchers) {
            ok($watchers[$i]->active, "watcher $i is active");
        }
        ok($agg->active, 'agg is active');
        is_deeply([$agg->watchers], \@watchers, "watchers OK");
        is(scalar($selector->watchers), 3, "3 active watchers");
        my $new_watcher = create_child($selector, $child_type);
        $new_watcher->cancel();
        ok(!$new_watcher->active, 'new_watcher is inactive');
        push(@watchers, $new_watcher);
        $agg->add($new_watcher);
        foreach my $i (0 .. $#watchers) {
            ok(!$watchers[$i]->active, "watcher $i is inactive");
        }
        ok(!$agg->active, "agg becomes inactive because an inactive watcher is added.");
        is_deeply([$agg->watchers], \@watchers, "watchers OK");
        is(scalar($selector->watchers), 0, "0 active watcher");
    }

    {
        my $selector = create_selector();
        my $agg = new_ok('Async::Selector::Aggregator');
        ok($agg->active, 'agg is active at first');
        $agg->cancel();
        ok(!$agg->active, 'agg becomes inactive although there is no watchers in it');
        my @watchers = ();
        my $new_watcher = create_child($selector, $child_type);
        ok($new_watcher->active, 'new_watcher is active');
        is(scalar($selector->watchers), 1, '1 active watcher');
        push(@watchers, $new_watcher);
        $agg->add($new_watcher);
        ok(!$new_watcher->active, 'new_watcher becomes inactive because it is added to an inactive aggregator');
        is_deeply([$agg->watchers], \@watchers, 'watchers OK');
        is(scalar($selector->watchers), 0, '0 active watcher');
    }
}



####################

{
    my $agg = new_ok('Async::Selector::Aggregator');
    ok($agg->active, "agg is active when it's empty");
    dies_ok { $agg->add($agg) } "it croaks when you try to add the agg itself.";
    $agg->cancel;
    ok(!$agg->active, "agg becomes inactive after cancel() even when it's empty");
    dies_ok { $agg->add($agg) } "it croaks when you try to add the agg itself.";
}

test_pure_children('watcher');
test_pure_children('aggregator');

note("--- junk tests");
foreach my $case (
    {label => "undef", junk => undef},
    {label => "number", junk => 10},
    {label => "string", junk => "hoge"},
    {label => "arrayref", junk => []},
    {label => "hashref", junk => {}},
    {label => "coderef", junk => sub {}},
    {label => "other object", junk => Async::Selector::Test::Dummy->new()}
) {
    my $agg = Async::Selector::Aggregator->new();
    dies_ok { $agg->add($case->{junk}) } "add() dies when adding a junk like $case->{label}";
}

{
    note('--- heavy aggregation chain');
    my $selector = create_selector();
    my @watchers = ();
    local *make_aggregation_tree = sub {
        my ($depth) = @_;
        if($depth <= 0) {
            my $w = create_child($selector, 'watcher');
            push(@watchers, $w);
            return $w;
        }
        my $agg = Async::Selector::Aggregator->new();
        my $w = create_child($selector, 'watcher');
        push(@watchers, $w);
        $agg->add($w);
        $agg->add(make_aggregation_tree($depth - 1));
        return $agg;
    };
    my $agg = make_aggregation_tree(5);
    is(scalar(@watchers), 6, "6 watchers in the tree");
    foreach my $i (0 .. $#watchers) {
        ok($watchers[$i]->active, "watcher $i is active");
    }
    ok($agg->active, "agg is active");
    is(scalar($selector->watchers), 6, '6 active watchers');

    $agg->cancel();
    is(scalar(@watchers), 6, "6 watchers in the tree");
    foreach my $i (0 .. $#watchers) {
        ok(!$watchers[$i]->active, "watcher $i becomes inactive");
    }
    ok(!$agg->active, "agg becomes inactive by cancel()");
    is(scalar($selector->watchers), 0, '0 active watchers');

    @watchers = ();
    $agg = make_aggregation_tree(5);
    ok($agg->active, 'agg is active');
    is(scalar($selector->watchers), 6, '6 active watchers');
    my $new_watcher = create_child($selector, 'watcher');
    $new_watcher->cancel();
    $agg->add($new_watcher);
    ok(!$agg->active, 'agg becomes inactive by adding inactive watcher');
    is(scalar($selector->watchers), 0, '0 active watcher');

    @watchers = ();
    $agg = Async::Selector::Aggregator->new();
    $agg->add(create_child($selector, 'watcher'));
    $agg->cancel();
    ok(!$agg->active, 'agg is inactive');
    my $new_agg = make_aggregation_tree(5);
    ok($new_agg->active, 'new_agg is active');
    is(scalar($selector->watchers), 6, '6 active watchers');
    $agg->add($new_agg);
    ok(!$new_agg->active, 'new_agg becomes inactive because it is added to an inactive aggregator');
    is(scalar($selector->watchers), 0, '0 active watchers');
}

done_testing();