The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl -w
use strict;

use Test::More;
use Data::Dumper;
use Scalar::Util qw(blessed);

unless (eval "use Test::NoWarnings; 1") {
  diag 'Please consider installing Test::NoWarnings for an additional test';
}

#################### prepare some subs

# object is "in sin", that is, not blessed.
sub in_sin($;$) {
  my ($obj, $comment) = @_;
  my $t = Test::More->builder;
  $t->ok(! blessed($obj), $comment);
}

sub create_beat {
  open my $fh, "<", "Makefile.PL" or die "Could not open Makefile.PL for testing";
  return bless({
            one => bless({
                    hey => 'ho',
                  }, 'AOne'),
            two => bless({
                    list => [
                      bless({ three => 3 }, 'AThree'),
                      bless({ four  => 4 }, 'AFour'),
                      5,
                     "this is just noise",
                    ],
                  }, 'ATwo'),
            six => {
              seven => bless({ __VALUE__ => 7}, 'ASeven'),
              eight => bless({ __VALUE__ => 8}, 'AnEight'),
            },
            func => sub { 100; },
            funcy => bless(sub { 42; }, 'AFunc'),
            deep => bless({
                      deeper => bless({
                                  deepest => bless({ field => "value" }, "Deepest"),
                      }, "Deeper"),
            }, "Deep"),
            #fh => bless $fh, "FileHandler",
            none => undef,
         }, 'AOne');
}


################################## Now let's start the tests

require_ok 'Class::Rebless';

{
  # rebase simple HASH
  my $empty = {};
  Class::Rebless->rebase($empty, 'Full');
  ok   ! UNIVERSAL::isa($empty, 'Full'), 'rebasing HASH does not do anything';

  # rebase simple HASH based class
  $empty = bless({}, 'Empty');
  isa_ok $empty, 'Empty', 'Before rebasing Empty';
  Class::Rebless->rebase($empty, 'Not');
  isa_ok $empty, 'Not::Empty', 'After rebasing Empty';
}

# rebasing nonblessed scalar reference
{
  my $foo = "bar";
  my $moo = \$foo;
  is     $foo, "bar";
  isa_ok $moo, "SCALAR";
  in_sin $moo;
  is     $$moo, "bar";

  Class::Rebless->rebase($moo, "And");
  isa_ok $moo, "SCALAR";
  in_sin $moo;
  is     $$moo, "bar";

  Class::Rebless->rebless($moo, "Else");
  isa_ok $moo, "SCALAR";
  in_sin $moo;
  is     $$moo, "bar";
}

# rebasing and reblessing blessed scalar reference
{
  my $foo = "Foo";
  my $moo = bless \$foo, "Foo";
  isa_ok $moo, "SCALAR";
  isa_ok $moo, "Foo";
  #is     $$moo, "bar"; #TODO

  Class::Rebless->rebase($moo, "And");
  isa_ok $moo, "SCALAR";
  isa_ok $moo, "And::Foo";
  #is     $$moo, "bar"; #TODO

  Class::Rebless->rebless($moo, "Else");
  isa_ok $moo, "SCALAR";
  isa_ok $moo, "Else";
  #is     $$moo, "bar"; #TODO
}

{
  open my $fh, "<", "Makefile.PL" or die;
  bless $fh, "FileHandler";
  isa_ok $fh, "GLOB";
  isa_ok $fh, "FileHandler";

  eval{Class::Rebless->rebase($fh, "And");};
  ok !$@, "rebase on filehandles lives";
  diag $@ if $@;

  isa_ok $fh, "GLOB";
  isa_ok $fh, "And::FileHandler";

  eval{Class::Rebless->rebless($fh, "NewFileHandler");};
  ok !$@, "rebless on filehandles lives";
  diag $@ if $@;

  isa_ok $fh, "GLOB";
  isa_ok $fh, "NewFileHandler";
}

{
  no strict;
  $foo = 42;
  @foo = (19, 23);
  %foo = (field => "value");
  my $bar = \*foo;
  bless $bar, "Bar";
  isa_ok $bar, "GLOB";
  isa_ok $bar, "Bar";
  is_deeply \@$$bar, [19, 23];
  is_deeply \%$$bar, {field => "value"};
  is $$$bar, 42, "sanity, got back 42 from GLOB";

  Class::Rebless->rebase($bar, 'And');

  isa_ok $bar, "GLOB";
  isa_ok $bar, "And::Bar";
  is $$$bar, 42, "got back 42 from GLOB";
  is_deeply \@$$bar, [19, 23];
  is_deeply \%$$bar, {field => "value"};
}


{
  my $beat = create_beat();

  # before changing
  isa_ok $beat, "AOne";
  isa_ok $beat->{one}, "AOne";
  isa_ok $beat->{two}, "ATwo";
  isa_ok $beat->{func}, "CODE";
  in_sin $beat->{func};
  isa_ok $beat->{funcy}, "CODE";
  isa_ok $beat->{funcy}, "AFunc";
  is     $beat->{func}->(), 100, "hundred";
  is     $beat->{funcy}->(), 42, "the answer";

  isa_ok $beat->{deep}, "HASH";
  isa_ok $beat->{deep}, "Deep";
  isa_ok $beat->{deep}{deeper}, "HASH";
  isa_ok $beat->{deep}{deeper}, "Deeper";
  isa_ok $beat->{deep}{deeper}{deepest}, "HASH";
  isa_ok $beat->{deep}{deeper}{deepest}, "Deepest";
  is     $beat->{deep}{deeper}{deepest}{field}, "value";
  in_sin $beat->{deep}{deeper}{deepest}{field};

  #isa_ok $beat->{fh}, "GLOB";
  #isa_ok $beat->{fh}, "FileHandler";
  #diag Dumper $beat;

  Class::Rebless->rebase($beat, 'And');
  #diag Dumper $beat;
  isa_ok $beat, "And::AOne";
  isa_ok $beat->{one}, "And::AOne";
  isa_ok $beat->{two}, "And::ATwo";
  in_sin $beat->{two}{list}, "two-list not blessed";

  isa_ok $beat->{two}{list}[0], "And::AThree";
  isa_ok $beat->{two}{list}[1], "And::AFour";
  in_sin $beat->{two}{list}[2], "two list 2 is not blessed";
  in_sin $beat->{two}{list}[3], "two list 3 is not blessed";

  in_sin $beat->{six}, "six is not blessed";

  isa_ok $beat->{six}{seven}, "And::ASeven";
  isa_ok $beat->{six}{eight}, "And::AnEight";

  in_sin $beat->{func};
  isa_ok $beat->{funcy}, "And::AFunc";

  isa_ok $beat->{deep}, "And::Deep";
  isa_ok $beat->{deep}{deeper}, "And::Deeper";
  isa_ok $beat->{deep}{deeper}{deepest}, "And::Deepest";
  in_sin $beat->{deep}{deeper}{deepest}{field};

  is_deeply($beat, create_beat(), "structure is the same");
}

# rebless complex structure
{
  my $beat = create_beat();

  Class::Rebless->rebless($beat, 'Beatless');
  #diag Dumper $beat;
  isa_ok $beat, "Beatless";
  isa_ok $beat->{one}, "Beatless";
  isa_ok $beat->{two}, "Beatless";
  in_sin $beat->{two}{list}, "two-list not blessed";

  isa_ok $beat->{two}{list}[0], "Beatless";
  isa_ok $beat->{two}{list}[1], "Beatless";
  in_sin $beat->{two}{list}[2], "two list 2 is not blessed";
  in_sin $beat->{two}{list}[3], "two list 3 is not blessed";

  in_sin $beat->{six}, "six is not blessed";

  isa_ok $beat->{six}{seven}, "Beatless";
  isa_ok $beat->{six}{eight}, "Beatless";

  in_sin $beat->{func};
  isa_ok $beat->{funcy}, "Beatless";

  isa_ok $beat->{deep}, "Beatless";
  isa_ok $beat->{deep}{deeper}, "Beatless";
  isa_ok $beat->{deep}{deeper}{deepest}, "Beatless";
  in_sin $beat->{deep}{deeper}{deepest}{field};

  is_deeply($beat, create_beat(), "structure is the same");
}

{
  my $beat = create_beat();

  Class::Rebless->custom($beat, 'Custom', { editor => \&my_custom_editor });

  isa_ok $beat, "Custom::AOne";
  isa_ok $beat->{one}, "Custom::AOne";
  isa_ok $beat->{two}, "Custom::ATwo";
  in_sin $beat->{two}{list}, "two-list not blessed";

  isa_ok $beat->{two}{list}[0], "Custom::Three3::AThree";
  isa_ok $beat->{two}{list}[1], "Custom::Four4::AFour";
  in_sin $beat->{two}{list}[2], "two list 2 is not blessed";
  in_sin $beat->{two}{list}[3], "two list 3 is not blessed";

  in_sin $beat->{six}, "six is not blessed";

  isa_ok $beat->{six}{seven}, "Custom";
  isa_ok $beat->{six}{eight}, "Custom::AnEight";

  in_sin $beat->{func};
  isa_ok $beat->{funcy}, "Custom::AFunc";

  isa_ok $beat->{deep}, "Deep"; # PRUNELESS
  isa_ok $beat->{deep}{deeper}, "Custom::Deeper";
  isa_ok $beat->{deep}{deeper}{deepest}, "Custom::Deepest";
  in_sin $beat->{deep}{deeper}{deepest}{field};

  is_deeply($beat, create_beat(), "structure is the same");
}

{
  my $beat = create_beat();

  ok !Class::Rebless->prune(), "no prune yet";
  Class::Rebless->prune("__MYPRUNE__");
  is Class::Rebless->prune(), "__MYPRUNE__", "we have prune now";
  Class::Rebless->custom($beat, 'Custom', { editor => \&my_custom_editor });

  isa_ok $beat, "Custom::AOne";
  isa_ok $beat->{one}, "Custom::AOne";
  isa_ok $beat->{two}, "Custom::ATwo";
  in_sin $beat->{two}{list}, "two-list not blessed";

  isa_ok $beat->{two}{list}[0], "Custom::Three3::AThree";
  isa_ok $beat->{two}{list}[1], "Custom::Four4::AFour";
  in_sin $beat->{two}{list}[2], "two list 2 is not blessed";
  in_sin $beat->{two}{list}[3], "two list 3 is not blessed";

  in_sin $beat->{six}, "six is not blessed";

  isa_ok $beat->{six}{seven}, "Custom";
  isa_ok $beat->{six}{eight}, "Custom::AnEight";

  in_sin $beat->{func};
  isa_ok $beat->{funcy}, "Custom::AFunc";

  # difference from previous because of prune:
  isa_ok $beat->{deep}, "Deep";
  isa_ok $beat->{deep}{deeper}, "Deeper";
  isa_ok $beat->{deep}{deeper}{deepest}, "Deepest";
  in_sin $beat->{deep}{deeper}{deepest}{field};

  is_deeply($beat, create_beat(), "structure is the same");
}

{
  my $structure = [ 1, [ 2, [ 3, ] ] ];

  {
    local $Class::Rebless::MAX_RECURSE = 3;
    local $@;
    my $ok = eval { Class::Rebless->rebless($structure, 'Bogus'); 1 };
    ok($ok, "we can recurse up to MAX_RECURSE times") or diag "error: $@";
  }

  {
    local $Class::Rebless::MAX_RECURSE = 2;
    local $@;
    my $ok = eval { Class::Rebless->rebless($structure, 'Bogus'); 1 };
    like $@, qr/maximum recursion level exceeded/, "...but no more";
  }
}

{
  my $obj_1 = bless {} => 'X';
  my $obj_2 = bless {} => 'X';

  $obj_1->{obj_2} = $obj_2;
  $obj_2->{obj_1} = $obj_1;

  Class::Rebless->rebless($obj_1, 'Foo');
}

# in an END{} because there's a previously-registered END{} for
# Test::NoWarnings, potentially -- rjbs, 2011-03-21
END { done_testing; }

sub my_custom_editor {
  my ($obj, $namespace) = @_;
  return bless $obj, $namespace . '::Three3::' . ref $obj if "AThree" eq ref $obj;
  return bless $obj, $namespace . '::Four4::' . ref $obj if "AFour" eq ref $obj;
  return bless $obj, $namespace if "ASeven" eq ref $obj;
  return "__MYPRUNE__" if "Deep" eq ref $obj;
  return bless $obj, $namespace . '::' . ref $obj;
}