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

BEGIN {
    # don't run without Scalar::Util::weaken()
    eval "use Scalar::Util 'weaken'";
    if( $@ =~ /\AWeak references are not implemented/ ) {
        require Test::More;
        Test::More::plan( skip_all =>
            "Can't test storable refs without Scalar::Util::weaken" );
    }
}

use Test::More;
use Class::InsideOut ();
use Scalar::Util qw( refaddr reftype weaken isweak );

# Need Storable 2.04 ( relatively safe STORABLE_freeze support )
eval { require Storable and Storable->VERSION( 2.04 ) };
if ( $@ ) {
    plan skip_all => "Storable >= 2.04 not installed",
}

sub check_version {
    my ($class, $version) = @_;
    eval { require $class and $class->VERSION($version) };
    return $@ eq q{} ? 0 : 1;
}
    
my @serializers = (
    {
        class   => "Storable",
        version => 3.04,
        freeze  => sub { Storable::freeze( shift ) },
        thaw    => sub { Storable::thaw( shift ) },
    },
);

my $tests_per_serializer = 68;
plan tests => @serializers * $tests_per_serializer;

my $class = "t::Object::Friends";

for my $s ( @serializers ) {

    SKIP:
    {
        skip "$s->{class} $s->{version} required", $tests_per_serializer
            unless check_version( $s->{class}, $s->{version} );

        require_ok( $s->{class} );

        # Setup test data and variables

        my ($alice, $bob, $charlie);
        my ($alice2, $bob2, $charlie2);
        my ( $frozen, $thawed );
        my @friends;

        # start tests

        require_ok( $class );

        # create the objects
        ok( $alice = $class->new( { name => "Alice" } ),  
            "Creating $class object 'Alice'"
        );

        ok( $bob = $class->new( { name => "Bob" } ),  
            "Creating $class object 'Bob'"
        );

        ok( $bob->friends( $alice ),
            "Making Bob friends with Alice"
        );

        ok( $charlie = $class->new( { name => "Charlie" } ),  
            "Creating $class object 'charlie'"
        );

        ok( $charlie->friends( $alice, $bob ),
            "Making Charlie friends with Alice and Bob"
        );

        ok( $charlie->has_friend( $bob ),
            "Confirming 'has_friend' method works"
        );

        # Freezing just Bob should clone Alice

        # freeze object
        ok( $frozen = $s->{freeze}->( $bob ),
            "Freezing Bob"
        );

        # thaw object
        ok( $bob2 = $s->{thaw}->( $frozen ),
            "... Thawing the frozen Bob"
        );
        is( ref $bob2, $class,
            "... Thawed Bob is a $class"
        );
        isnt( refaddr $bob2, refaddr $bob,
            "... Thawed Bob is a new object"
        );

        # check name
        is( $bob2->name(), "Bob",
            "... Thawed Bob is also named Bob (hereafter Bob2)"
        );

        # check reference copy
        ok( ! $bob2->has_friend( $alice ),
            "... Bob2 is not friends with Alice"
        );

        is( @friends = $bob2->friends, 1,
            "... Bob2 still has 1 friend"
        );

        isa_ok( $friends[0], $class, 
            "... Bob2's friend"
        );
        

        is( $friends[0]->name, "Alice",
            "... Bob2's friend is also named 'Alice'"
        );

        # Freezing Bob and Alice together should preserve relationship

        # freeze object
        ok( $frozen = $s->{freeze}->( [ $bob, $alice ] ),
            "Freezing Bob and Alice together"
        );


        # thaw object
        ($bob2, $alice2) = @{ $s->{thaw}->( $frozen ) };

        pass(
            "... Thawing the frozen Bob and Alice"
        );

        is( ref $bob2, $class,
            "... Thawed Bob is a $class"
        );
        is( ref $alice2, $class,
            "... Thawed Alice is a $class"
        );
        isnt( refaddr $bob2, refaddr $bob,
            "... Thawed Bob is a new object"
        );

        # check name
        is( $bob2->name(), "Bob",
            "... Thawed Bob is also named Bob (hereafter Bob2)"
        );

        isnt( refaddr $bob2, refaddr $bob,
            "... Bob2 is not Bob"
        );

        is( $alice2->name(), "Alice",
            "... Other thawed object is named Alice (hereafter Alice2)"
        );

        isnt( refaddr $alice2, refaddr $alice,
            "... Alice2 is not Alice"
        );

        # check reference copy
        ok( ! $bob2->has_friend( $alice ),
            "... Bob2 is not friends with Alice"
        );

        is( @friends = $bob2->friends, 1,
            "... Bob2 still has 1 friend"
        );

        is( refaddr $friends[0], refaddr $alice2,
            "... Bob2's friend is Alice2"
        );

        # Freezing Charlie and Bob and Alice together should preserve all 
        # relationships

        # freeze object
        ok( $frozen = $s->{freeze}( [ $bob, $alice, $charlie ] ),
            "Freezing Charlie, Bob and Alice together"
        );

        # thaw object
        ($bob2, $alice2, $charlie2) = @{ $s->{thaw}->( $frozen ) };

        pass(
            "... Thawing the frozen Charlie, Bob and Alice"
        );
        
        is( ref $charlie2, $class,
            "... Thawed Bob is a $class"
        );
        is( ref $bob2, $class,
            "... Thawed Bob is a $class"
        );
        is( ref $bob2, $class,
            "... Thawed Alice is a $class"
        );
        isnt( refaddr $bob2, refaddr $bob,
            "... Thawed Bob is a new object"
        );


        # check name
        is( $charlie2->name(), "Charlie",
            "... One thawed object is also named Charlie (hereafter Charlie2)"
        );

        isnt( refaddr $charlie2, refaddr $charlie,
            "... Charlie2 is not Charlie"
        );

        is( $bob2->name(), "Bob",
            "... Another thawed object is also named Bob (hereafter Bob2)"
        );

        isnt( refaddr $bob2, refaddr $bob,
            "... Bob2 is not Bob"
        );

        is( $alice2->name(), "Alice",
            "... Another thawed object is named Alice (hereafter Alice2)"
        );

        isnt( refaddr $alice2, refaddr $alice,
            "... Alice2 is not Alice"
        );

        # check reference copy
        ok( ! $bob2->has_friend( $alice ),
            "... Bob2 is not friends with Alice"
        );

        ok( ! $charlie2->has_friend( $alice ),
            "... Charlie2 is not friends with Alice"
        );

        ok( ! $charlie2->has_friend( $bob ),
            "... Charlie2 is not friends with Bob"
        );

        is( @friends = $charlie2->friends, 2,
            "... Charlie2 still has 2 friends"
        );

        ok( $charlie2->has_friend( $alice2 ),
            "... Charlie2 has Alice2 as a friend"
        );

        ok( $charlie2->has_friend( $bob2 ),
            "... Charlie2 has Bob2 as a friend"
        );

        ok( $bob2->has_friend( $alice2 ),
            "... Bob2 has Alice2 as a friend"
        );

        # storing Alice inside herself !!

        push @$alice, $alice;
        weaken( $alice->[0] );
        ok( isweak( $alice->[0] ),
            "Storing a weak reference to Alice inside Alice (!!)"
        );

        # freeze object
        ok( $frozen = $s->{freeze}->( $alice ),
            "Freezing Alice"
        );

        # thaw object
        ok( $alice2 = $s->{thaw}( $frozen ),
            "... Thawing the frozen Alice as Alice2"
        );

        is( ref $alice2, $class,
            "... Thawed Alice is a $class"
        );

        is( $alice2->[0], $alice2,
            "... Found Alice2 inside Alice2 (Lewis Carroll eat your heart out!)"
        );

        ok( ! isweak( $alice2->[0] ),
            "... Reference to Alice2 isn't weak -- limitation of Storable"
        );

        shift @$alice;
        is( @$alice, 0,
            "Removing Alice from herself"
        );

            
        # let's make alice a narcissist and clone her!

        ok( $alice->friends( $alice ),
            "Making Alice friends with herself (!!)"
        );

        # freeze object
        ok( $alice2 = $s->{thaw}->( $s->{freeze}->( $alice ) ),
            "Cloning Alice into Alice2 (with dclone)"
        );

        is( ref $alice2, $class,
            "... Thawed Alice is a $class"
        );

        isnt( refaddr $alice2, refaddr $alice,
            "... Alice2 is a new object"
        );

        # check reference copy
        ok( ! $alice2->has_friend( $alice ),
            "... Alice2 is not friends with Alice"
        );

        ok( $alice2->has_friend( $alice2 ),
            "... Alice2 is friends with Alice2"
        );

        # Bilateral friendship between Alice and Bob

        $alice->friends( undef );
        is( scalar $alice->friends, 0,
            "Alice is no longer friends with herself (try therapy?)"
        );
            
        ok( $alice->friends( $bob ),
            "Making Alice friends with Bob"
        );

        # freeze object
        ok( $alice2 = $s->{thaw}->( $s->{freeze}->( $alice ) ),
            "Cloning Alice into Alice2 (with dclone)"
        );

        is( ref $bob2, $class,
            "... Thawed Alice is a $class"
        );

        ok( ! $alice2->has_friend( $bob ),
            "... Alice2 is not friends with Bob"
        );

        ($bob2) = $alice2->friends;

        is( $bob2->name, "Bob",
            "... Alice2 does have a friend named Bob (hereafter Bob2)"
        );

        ok( $bob2->has_friend( $alice2 ),
            "... Bob2 is friends with Alice2"
        );

    }
}