#!/usr/bin/perl

use strict;
use warnings;

use Test::More tests => 94;

BEGIN { 
    use_ok('Class::Cloneable');
}

{ # basic object, with no cloning
    package ObjectWithoutClone; 
    sub new { bless { no_clone => 1 } }
}

{ # basic object with its own clone method
    package ObjectWithClone;
    sub new   { bless { clone => 1 } }
    sub clone { bless { clone => 1 } }
}

{ # basic cloneable subclass
    package CloneableObject;
    our @ISA = ('Class::Cloneable');
    sub new { bless { cloneable => 1 } }
}

{ # Tied Hash test
    package TiedHashTest;
    # copied straight from Tie::StdHash
    sub TIEHASH  { bless {}, $_[0] }
    sub STORE    { $_[0]->{$_[1]} = $_[2] }
    sub FETCH    { $_[0]->{$_[1]} }
    sub FIRSTKEY { my $a = scalar keys %{$_[0]}; each %{$_[0]} }
    sub NEXTKEY  { each %{$_[0]} }
    sub EXISTS   { exists $_[0]->{$_[1]} }
    sub DELETE   { delete $_[0]->{$_[1]} }
    sub CLEAR    { %{$_[0]} = () }
    sub SCALAR   { scalar %{$_[0]} }   
}

{ # Tied Hash test
    package TiedArrayTest;
    # copied straight from Tie::StdArray
    sub TIEARRAY  { bless [], $_[0] }
    sub FETCHSIZE { scalar @{$_[0]} }
    sub STORESIZE { $#{$_[0]} = $_[1]-1 }
    sub STORE     { $_[0]->[$_[1]] = $_[2] }
    sub FETCH     { $_[0]->[$_[1]] }
    sub CLEAR     { @{$_[0]} = () }
    sub POP       { pop(@{$_[0]}) }
    sub PUSH      { my $o = shift; push(@$o,@_) }
    sub SHIFT     { shift(@{$_[0]}) }
    sub UNSHIFT   { my $o = shift; unshift(@$o,@_) }
    sub EXISTS    { exists $_[0]->[$_[1]] }
    sub DELETE    { delete $_[0]->[$_[1]] }
    sub EXTEND    {}
    sub SPLICE    {}
}

{ # Tied Hash test
    package TiedScalarTest;
    sub TIESCALAR { my $var; bless \$var, $_[0] }    
    sub FETCH   { ${$_[0]} }
    sub STORE   { ${$_[0]} = $_[1] }
    sub DESTROY { undef ${$_[0]} }
}

{ # test cloneable object
    package CloneableTest;
    our @ISA = ('Class::Cloneable');
    
    sub new {
        my ($class) = @_;
        my $scalar = "Test";
        my %hash_to_tie   = ( tied_hash => 1 );
        tie %hash_to_tie, 'TiedHashTest';
        my @array_to_tie  = (1, 2, 3, 4);
        tie @array_to_tie, 'TiedArrayTest';
        my $scalar_to_tie; 
        tie $scalar_to_tie, 'TiedScalarTest';
        $scalar_to_tie = "Tie Me";
        my $cloneable = bless {
            hash             => { one => 1 },
            array            => [ 1, 2, 3 ],
            scalar_ref       => \$scalar,
            scalar           => "Test",
            nested_hash      => { level_one => { level_two => { level_three => { level_four => undef }}}},
            nested_array     => [ 1, [ 2, [ 3, [ 4 ]]]],
            tied_hash        => \%hash_to_tie,
            tied_array       => \@array_to_tie,
            tied_scalar      => \$scalar_to_tie,
            code_ref         => sub { "hello" },
            regexp_ref       => qr/(.*?)/,
            glob_ref         => \*new,
            object_wo_clone  => ObjectWithoutClone->new(),
            object_w_clone   => ObjectWithClone->new(),
            cloneable_object => CloneableObject->new()
            }, $class;
        $cloneable->{ref_to_ref} = \$cloneable->{scalar_ref};    
        return $cloneable;
    }
}

{ # test cloneable object w/ overloading
    package OverloadedCloneableTest;
    our @ISA = ('CloneableTest');
    use overload '""' => "toString";
    sub toString { "This is my overloaded stringification method" }
}

# clone testing function
sub test_clone {
    my ($test, $clone) = @_;
    isnt($test->{hash},         $clone->{hash}, '... shallow hash clone was successful');
    is_deeply($test->{hash},    $clone->{hash}, '... shallow hash clone matches original');
    
    isnt($test->{array},        $clone->{array}, '... shallow array clone was successful');
    is_deeply($test->{array},   $clone->{array}, '... shallow array clone matches original');
    
    isnt($test->{scalar_ref},   $clone->{scalar_ref}, '... scalar ref clone was successful');
    is(${$test->{scalar_ref}},  ${$clone->{scalar_ref}}, '... scalar ref clone was successful');
        
    isnt($test->{ref_to_ref},     $clone->{ref_to_ref}, '... ref of ref clone was successful');    
    is(${${$test->{ref_to_ref}}}, ${${$clone->{ref_to_ref}}}, '... ref of ref clone matches original');    
        
    is($test->{scalar},         $clone->{scalar},     '... scalar clone was successful');
    
    isnt($test->{nested_hash},       $clone->{nested_hash}, '... nested hash clone was successful');
    is_deeply($test->{nested_hash},  $clone->{nested_hash}, '... nested hash clone matches original');
    
    isnt($test->{nested_array},      $clone->{nested_array}, '... nested array clone was successful');
    is_deeply($test->{nested_array}, $clone->{nested_array}, '... nested array clone matches original');
    
    isnt($test->{tied_hash},   $clone->{tied_hash}, '... tied hash clone was successful');
    ok(tied(%{$clone->{tied_hash}}), '... tied hash clone was successful');
    is(ref(tied(%{$clone->{tied_hash}})), 'TiedHashTest', '... tied hash clone was successful');
    
    isnt($test->{tied_array},  $clone->{tied_array}, '... tied array clone was successful');
    ok(tied(@{$clone->{tied_array}}), '... tied array clone was successful');
    is(ref(tied(@{$clone->{tied_array}})), 'TiedArrayTest', '... tied array clone was successful');

    isnt($test->{tied_scalar}, $clone->{tied_scalar}, '... tied scalar clone was successful');
    ok(tied(${$clone->{tied_scalar}}), '... tied scalar clone was successful');
    is(ref(tied(${$clone->{tied_scalar}})), 'TiedScalarTest', '... tied scalar clone was successful');
    
    is($test->{code_ref},        $clone->{code_ref},   '... code ref clone was successful');
    is($test->{regexp_ref},      $clone->{regexp_ref}, '... regexp ref clone was successful');
    is($test->{glob_ref},        $clone->{glob_ref},   '... glob ref clone was successful');
    
    is($test->{object_wo_clone}, $clone->{object_wo_clone}, '... object w/out clone method clone was successful');
    
    isnt($test->{object_w_clone},   $clone->{object_w_clone},   '... object with clone method clone was successful');
    isnt($test->{cloneable_object}, $clone->{cloneable_object}, '... Class::Cloneable clone was successful');
}

## TESTS

# now test the base cloneable
{
    can_ok("CloneableTest", 'new');
    my $test = CloneableTest->new();
    isa_ok($test, 'CloneableTest');
    isa_ok($test, 'Class::Cloneable');
    can_ok($test, 'clone');
    my $clone = $test->clone();
    test_clone($test, $clone);
}

# test it with an overloaded base object
{
    can_ok("OverloadedCloneableTest", 'new');
    my $test = OverloadedCloneableTest->new();
    isa_ok($test, 'OverloadedCloneableTest');
    isa_ok($test, 'CloneableTest');
    isa_ok($test, 'Class::Cloneable');
    can_ok($test, 'clone');
    my $clone = $test->clone();
    test_clone($test, $clone);
}

# test all the exceptions
{
    can_ok("CloneableTest", 'new');
    my $test = CloneableTest->new();
    isa_ok($test, 'CloneableTest');
    isa_ok($test, 'Class::Cloneable');
         
    eval {
        Class::Cloneable::Util::clone();
    };
    like($@, qr/Illegal Operation \: This method can only be called by a subclass of Class\:\:Cloneable/, 
         '... got the error we expected');
         
    eval {
        Class::Cloneable::Util::deconstructObject()
    };
    like($@, qr/Illegal Operation \: This method can only be called by a subclass of Class\:\:Cloneable/, 
         '... got the error we expected');         
         
    eval {
        Class::Cloneable::Util::cloneObject();
    };
    like($@, qr/Illegal Operation \: This method can only be called by a subclass of Class\:\:Cloneable/, 
         '... got the error we expected');   
         
    eval {
        Class::Cloneable::Util::cloneRef();
    };
    like($@, qr/Illegal Operation \: This method can only be called by a subclass of Class\:\:Cloneable/, 
         '... got the error we expected'); 
         
    {
        package CloneableExceptionTest;
        
        sub clone { Class::Cloneable::Util::clone(@_) }
        sub deconstructObject { Class::Cloneable::Util::deconstructObject(@_) }
        sub cloneObject { Class::Cloneable::Util::cloneObject(@_) }
        sub cloneRef { Class::Cloneable::Util::cloneRef(@_) }
    }
    
    eval {
        CloneableExceptionTest::clone();
    };
    like($@, qr/Illegal Operation \: This method can only be called by a subclass of Class\:\:Cloneable/, 
         '... got the error we expected');
         
    eval {
        CloneableExceptionTest::deconstructObject()
    };
    like($@, qr/Illegal Operation \: This method can only be called by a subclass of Class\:\:Cloneable/, 
         '... got the error we expected');
         
    eval {
        CloneableExceptionTest::cloneObject();
    };
    like($@, qr/Illegal Operation \: This method can only be called by a subclass of Class\:\:Cloneable/, 
         '... got the error we expected');   
         
    eval {
        CloneableExceptionTest::cloneRef();
    };
    like($@, qr/Illegal Operation \: This method can only be called by a subclass of Class\:\:Cloneable/, 
         '... got the error we expected');  
         
    {
        package CloneableArgumentExceptionTest;
        our @ISA = ('Class::Cloneable');
        
        sub clone { Class::Cloneable::Util::clone(@_) }
        sub deconstructObject { Class::Cloneable::Util::deconstructObject(@_) }
        sub cloneObject { Class::Cloneable::Util::cloneObject(@_) }
        sub cloneRef { Class::Cloneable::Util::cloneRef(@_) }
    }
    
    eval {
        CloneableArgumentExceptionTest::clone(undef);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone/, 
         '... got the error we expected');
         
    eval {
        CloneableArgumentExceptionTest::deconstructObject(undef)
    };
    like($@, qr/Insufficient Arguments \: Must specify a valid object to deconstruct/, 
         '... got the error we expected');
         
    eval {
        CloneableArgumentExceptionTest::deconstructObject("Fail")
    };
    like($@, qr/Insufficient Arguments \: Must specify a valid object to deconstruct/, 
         '... got the error we expected');    
         
    eval {
        CloneableArgumentExceptionTest::deconstructObject([])
    };
    like($@, qr/Insufficient Arguments \: Must specify a valid object to deconstruct/, 
         '... got the error we expected');                   
         
    eval {
        CloneableArgumentExceptionTest::cloneObject(undef, undef);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');  
         
    eval {
        CloneableArgumentExceptionTest::cloneObject("Fail", undef);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');   

    eval {
        CloneableArgumentExceptionTest::cloneObject([], undef);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');    
         
    eval {
        CloneableArgumentExceptionTest::cloneObject([], "Fail");
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');                    
         
    eval {
        CloneableArgumentExceptionTest::cloneObject([], []);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');                                                             
         
    eval {
        CloneableArgumentExceptionTest::cloneRef(undef, undef);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');  
         
    eval {
        CloneableArgumentExceptionTest::cloneRef("Fail", undef);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');   

    eval {
        CloneableArgumentExceptionTest::cloneRef([], undef);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');    
         
    eval {
        CloneableArgumentExceptionTest::cloneRef([], "Fail");
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');                    
         
    eval {
        CloneableArgumentExceptionTest::cloneRef([], []);
    };
    like($@, qr/Insufficient Arguments \: Must specify the object to clone and a valid cache/, 
         '... got the error we expected');
         
    # now just test some general weirdness
    # mostly for the sake of code coverage
   
    is_deeply(
        CloneableArgumentExceptionTest::clone("Test"),
        "Test",
        '... cloned as expected');     
      
    is_deeply(
        CloneableArgumentExceptionTest::clone([]),
        [],
        '... cloned as expected');     

    my $misc_obj = bless({}, 'Test');
    is(CloneableArgumentExceptionTest::clone($misc_obj),
        $misc_obj,
        '... cloned as expected');     
                    
}