The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

use strict;
use warnings;
use UR;
use IO::File;

use Test::More tests => 57;

UR::Object::Type->define(
    class_name => 'Circle',
    has => [
        radius => {
            is => 'Number',
            default_value => 1,
        },
    ],
);

sub add_test_observer {
    my ($aspect, $context, $observer_ran_ref) = @_;
    $$observer_ran_ref = 0;

    my $observer;
    my $callback;
    $callback = sub { $$observer_ran_ref = 1; };
    $observer = $context->add_observer(
        aspect => $aspect,
        callback => $callback,
    );

    unless ($observer) {
        die "Failed to add $aspect observer!";
    }
    return $observer;
}

# Create a Circle
my $circle = Circle->create();
ok($circle->isa('Circle'), 'create a circle');
ok($circle->radius == 1, 'default radius is 1');



# Verify Transaction Rollback Removes Observer and its Subscription
# making sure if someone tries to catch their observer's delete that it runs
# before the observer's self-created delete subscription
{
    my $ran_observer_observer = 0;

    my $circle_trans = UR::Context::Transaction->begin();
    ok($circle_trans, 'begin transaction');
    my $ran_circle_radius_observer = 0;
    my $circle_obs = $circle->add_observer(
        aspect => 'radius',
        callback => sub { $ran_circle_radius_observer = 1; },
    );
    my $circle_obs_id = $circle_obs->id;

    my $ran_circle_obs_delete_obs = 0;
    my $subscription = $circle_obs->class->create_subscription(
        id => $circle_obs->id,
        method => 'delete',
        callback => sub { $ran_circle_obs_delete_obs = 1; },
        note => "$circle_obs",
    );
    my $observer_observer = UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $subscription->[1]);

    ok($circle_obs->isa('UR::Observer'), 'added an observer on the circle');
    is(UR::Observer->get(subject_class_name => 'Circle', subject_id => $circle->id, aspect => 'radius'),
       $circle_obs,
       'Can get the observer on the circle with get()');
    my $circle_sub = $UR::Context::all_change_subscriptions->{Circle}->{radius}->{$circle->id};
    ok($circle_sub, 'adding observer inserted a callback into the Context data structure for callbacks');

    is(UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $circle_obs->id, aspect => 'delete'),
       $observer_observer,
       'Can get the observer on the original observer deletion with get()');

    ok($circle_trans->rollback(), 'rolled back transaction');

    ok($ran_circle_obs_delete_obs == 0, 'rollback did not run the delete observer');  # because it's creation was undone before the radius observer was deleted
    $circle_sub = $UR::Context::all_change_subscriptions->{Circle}->{radius}->{$circle->id};
    ok(!$circle_sub, 'rolling back transaction (and with it the observer) removed the subscription');
    ok($circle_obs->isa('UR::DeletedRef'), 'radius observer is now a DeletedRef');

    ok(! UR::Observer->get(subject_class_name => 'Circle', subject_id => $circle->id, aspect => 'radius'),
       'get() no longer returns the circle observer');
    ok(! UR::Observer->get(subject_class_name => 'UR::Observer', subject_id => $circle_obs_id, aspect => 'delete'),
       'get() no longer returns the observer observer');

    $ran_circle_obs_delete_obs = 0;
    $circle->radius(1);
    is($ran_circle_obs_delete_obs, 0, 'The circle radius observer did not run');
};



# Verify Transaction Rollback Observer Runs
{
    $circle->radius(3);
    ok($circle->radius == 3, "original radius is three");
    my $transaction = UR::Context::Transaction->begin();
    my $observer_ran = 0;
    add_test_observer('rollback', $transaction, \$observer_ran);
    my $sub = $UR::Context::all_change_subscriptions->{'UR::Context::Transaction'}->{rollback}->{$transaction->id};
    ok($sub, 'adding observer also create change subscription');
    ok($transaction->isa('UR::Context::Transaction'), "created first transaction (to test rollback observer)");
    ok(!$observer_ran, "observer rollback flag reset to 0");
    $circle->radius(5);
    ok($circle->radius == 5, "in transaction (rollback test), radius is five");
    ok($transaction->rollback(), "ran transaction rollback");
    ok($observer_ran, "rollback observer ran successfully");
    ok($circle->radius == 3, "after rollback, radius is three");
};



# Verify Transaction Commit Observer Runs
{
    $circle->radius(4);
    ok($circle->radius == 4, "original radius (commit test) is four");
    my $transaction = UR::Context::Transaction->begin();
    my $observer_ran = 0;
    add_test_observer('commit', $transaction, \$observer_ran);
    ok($transaction->isa('UR::Context::Transaction'), "created second transaction (to test commit observer)");
    ok(!$observer_ran, "observer rollback flag reset to 0");
    $circle->radius(6);
    ok($circle->radius == 6, "in transaction (commit test), radius is six");
    ok($transaction->commit(), "ran transaction commit");
    ok($observer_ran, "commit observer ran successfully");
    ok($circle->radius == 6, "after commit, radius is six");

    # Trying to Rollback a Committed Transaction Fails
    ok($transaction->state eq 'committed', "transaction is already committed");
    my $rv= eval {$transaction->rollback()} || 0;
    ok($rv == 0, "properly failed transaction rollback for already committed transaction");
};



# Test Nested Transactions
{
    $circle->radius(3);
    ok($circle->radius == 3, "original radius is 3");
    my $outer_transaction = UR::Context::Transaction->begin();
    my $outer_observer_ran = 0;
    add_test_observer('rollback', $outer_transaction, \$outer_observer_ran);
    ok($outer_transaction->isa('UR::Context::Transaction'), "created outer transaction");
    ok(!$outer_observer_ran, "outer observer flag reset to 0");
    $circle->radius(5);
    ok($circle->radius == 5, "in outer transaction, radius is 5");
    my $inner_transaction = UR::Context::Transaction->begin();
    my $inner_observer_ran = 0;
    add_test_observer('rollback', $inner_transaction, \$inner_observer_ran);
    ok($inner_transaction->isa('UR::Context::Transaction'), "created inner transaction");
    ok(!$inner_observer_ran, "inner observer flag reset to 0");
    $circle->radius(7);
    ok($circle->radius == 7, "in inner transaction, radius is 7");
    ok($inner_transaction->rollback(), "ran inner transaction rollback");
    ok($inner_observer_ran, "inner transaction observer ran successfully");
    ok($circle->radius == 5, "after inner transaction rollback, radius is 5");
    ok($outer_transaction->rollback(), "ran transaction rollback");
    ok($outer_observer_ran, "outer transaction observer ran successfully");
    ok($circle->radius == 3, "after rollback, radius is 3");
};

# testing inner commit
{
    $circle->radius(4);
    ok($circle->radius == 4, "original radius is 4");
    my $outer_transaction = UR::Context::Transaction->begin();
    my $outer_observer_ran = 0;
    add_test_observer('rollback', $outer_transaction, \$outer_observer_ran);
    ok($outer_transaction->isa('UR::Context::Transaction'), "created outer transaction");
    ok(!$outer_observer_ran, "outer observer flag reset to 0");
    $circle->radius(6);
    ok($circle->radius == 6, "in outer transaction, radius is 6");
    my $inner_transaction = UR::Context::Transaction->begin();
    my $inner_observer_ran = 0;
    add_test_observer('commit', $inner_transaction, \$inner_observer_ran);
    ok($inner_transaction->isa('UR::Context::Transaction'), "created inner transaction");
    ok(!$inner_observer_ran, "inner observer flag reset to 0");
    $circle->radius(8);
    ok($circle->radius == 8, "in inner transaction, radius is 8");
    ok($inner_transaction->commit(), "ran inner transaction commit");
    ok($inner_observer_ran, "inner transaction observer ran successfully");
    ok($circle->radius == 8, "after inner transaction commit, radius is 8");
    ok($outer_transaction->rollback(), "ran transaction rollback");
    ok($outer_observer_ran, "outer transaction observer ran successfully");
    ok($circle->radius == 4, "after rollback, radius is 4");
};

done_testing();

1;