use strict;
use warnings;

BEGIN {
    if ($] < 5.008) {
        print("1..0 # Skip :lvalue requires Perl 5.8.0 or later\n");
        exit(0);
    }
    eval { require Want; };
    if ($@ || $Want::VERSION < 0.12) {
        print("1..0 # Skip Needs Want v0.12 or later\n");
        exit(0);
    }
}

use Test::More 'no_plan';
use Scalar::Util;

package Baz; {
    use Object::InsideOut;
    sub me
    {
        my $self = shift;
        return ("Baz($$self)");
    }
}

package Foo; {
    use Object::InsideOut;

    # Separate get and set accessors
    my @foo1 :Field('Std' => 'foo1',                          'LValue' => 1, 'Return' => 'NEW');
    my @foo2 :Field('Get' => 'get_foo2', 'set' => 'set_foo2', 'lv'     => 1, 'Return' => 'OLD');
    my @foo3 :Field('STANDARD' => 'foo3',                     'LVALUE' => 1, 'Return' => 'SELF');

    # Combined get+set accessor
    my @bar1 :Field('LValue' => 'bar1',                'Return' => 'new');
    my @bar2 :Field('Acc'    => 'bar2', 'lvalue' => 1, 'Return' => 'Prev');
    my @bar3 :Field('get_set' => 'bar3', 'lv'    => 1, 'Return' => 'obj');

    # Type checking
    my @baz1 :Field('lv' => 'baz1', 'Return' => 'new', 'Type' => 'Baz');
    my @baz2 :Field('lv' => 'baz2', 'Return' => 'old', 'Type' => 'Baz');
    my @baz3 :Field('lv' => 'baz3', 'Return' => 'obj', 'Type' => 'Baz');

    my @num1 :Field('lv' => 'num1', 'Return' => 'new', 'Type' => 'num');
    my @num2 :Field('lv' => 'num2', 'Return' => 'old', 'Type' => 'num');
    my @num3 :Field('lv' => 'num3', 'Return' => 'obj', 'Type' => 'num');

    my @bork
        :Field
        :Type(Array_Ref(HASH))
        :LV(bork);

    sub me
    {
        my $self = shift;
        return ("Foo($$self)");
    }
}

package main;

sub change_it
{
    $_[0] = $_[1];
}

sub check_it
{
    my ($x, $y) = @_;
    if ($x eq $y) {
        ok(1, 'Checked');
    } else {
        is($x, $y, 'Check failed');
    }
}

MAIN:
{
    my $b1 = Baz->new();
    my $b2 = Baz->new();
    my $obj = Foo->new();
    ok($b1 && $b2 && $obj, 'Objects created');

    can_ok($obj, qw(new clone DESTROY CLONE
                    get_foo1 set_foo1 get_foo2 set_foo2 get_foo3 set_foo3
                    bar1 bar2 bar3));

    # set - return new
    eval { $obj->set_foo1(); };
    like($@, qr/Missing arg/            => 'rvalue set needs arg');

    $obj->set_foo1('val');
    is($obj->get_foo1(), 'val'          => 'rvalue set void');

    eval { $obj->get_foo1(); };
    is($@, ''                           => 'rvalue get void');

    my $val = $obj->set_foo1($b1);
    is($val, $b1                        => 'rvalue set returns new');

    my $val2 = $obj->get_foo1();
    is($val2, $b1                       => 'rvalue get');

    $obj->set_foo1() = $b2;
    is($obj->get_foo1(), $b2            => 'lvalue assign');

    $obj->set_foo1('foo') = 'Bert';
    is($obj->get_foo1(), 'Bert'         => 'lvalue assign (arg ignored)');

    $obj->set_foo1() =~ s/er/re/;
    is($obj->get_foo1(), 'Bret'         => 'lvalue re');

    change_it($obj->set_foo1(), 'Fred');
    is($obj->get_foo1(), 'Fred'         => 'lvalue');
    check_it($obj->set_foo1(), 'Fred');

    change_it($obj->set_foo1('Bert'), 'Mike');
    is($obj->get_foo1(), 'Mike'         => 'lvalue + arg new');
    check_it($obj->set_foo1('Ralph'), 'Ralph');

    $obj->set_foo1($b1);
    eval { $val = $obj->set_foo1()->me(); };
    like($@, qr/Missing arg/            => 'chain set needs arg');

    $val = $obj->set_foo1('bork')->me();
    is($val, 'Foo(1)'                   => 'chain self');

    $val = $obj->set_foo1($b2)->me();
    is($val, 'Baz(2)'                   => 'chain new object');


    # set - return old
    eval { $obj->set_foo2(); };
    like($@, qr/Missing arg/            => 'rvalue set needs arg');

    $obj->set_foo2('val');
    is($obj->get_foo2(), 'val'          => 'rvalue set void');

    eval { $obj->get_foo2(); };
    is($@, ''                           => 'rvalue get void');

    $obj->set_foo2($b2);
    $val = $obj->set_foo2($b1);
    is($val, $b2                        => 'rvalue set returns old');

    $val2 = $obj->get_foo2();
    is($val2, $b1                       => 'rvalue get');

    $obj->set_foo2() = $b2;
    is($obj->get_foo2(), $b2            => 'lvalue assign');

    $obj->set_foo2('foo') = 'Bert';
    is($obj->get_foo2(), 'Bert'         => 'lvalue assign (arg ignored)');

    $obj->set_foo2() =~ s/er/re/;
    is($obj->get_foo2(), 'Bret'         => 'lvalue re');

    change_it($obj->set_foo2(), 'Fred');
    is($obj->get_foo2(), 'Fred'         => 'lvalue');
    check_it($obj->set_foo2(), 'Fred');

    change_it($obj->set_foo2('Bert'), 'Mike');
    is($obj->get_foo2(), 'Bert'         => 'lvalue + arg old');
    check_it($obj->set_foo2('Ralph'), 'Bert');

    $obj->set_foo2($b1);
    eval { $val = $obj->set_foo2()->me(); };
    like($@, qr/Missing arg/            => 'chain set needs arg');

    $obj->set_foo2('bork');
    $val = $obj->set_foo2('bar')->me();
    is($val, 'Foo(1)'                   => 'chain self');

    $obj->set_foo2($b1);
    $val = $obj->set_foo2($b2)->me();
    is($val, 'Baz(1)'                   => 'chain old object');


    # set - return self
    eval { $obj->set_foo3(); };
    like($@, qr/Missing arg/            => 'rvalue set needs arg');

    $obj->set_foo3('val');
    is($obj->get_foo3(), 'val'          => 'rvalue set void');

    eval { $obj->get_foo3(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->set_foo3($b1);
    is($val, $obj                       => 'rvalue set returns self');

    $val2 = $obj->get_foo3();
    is($val2, $b1                       => 'rvalue get');

    $obj->set_foo3() = $b2;
    is($obj->get_foo3(), $b2            => 'lvalue assign');

    $obj->set_foo3('foo') = 'Bert';
    is($obj->get_foo3(), 'Bert'         => 'lvalue assign (arg ignored)');

    $obj->set_foo3() =~ s/er/re/;
    is($obj->get_foo3(), 'Bret'         => 'lvalue re');

    change_it($obj->set_foo3(), 'Fred');
    is($obj->get_foo3(), 'Fred'         => 'lvalue');
    check_it($obj->set_foo3(), 'Fred');

    my $obj_old = $obj;
    change_it($obj->set_foo3('Bert'), 'Mike');
    is($obj, 'Mike'                     => 'lvalue + arg self');
    $obj = $obj_old;
    is($obj->get_foo3(), 'Bert'         => 'Change did set');
    check_it($obj->set_foo3('Ralph'), $obj);
    is($obj->get_foo3(), 'Ralph'        => 'Check did set');

    $obj->set_foo3($b1);
    eval { $val = $obj->set_foo3()->me(); };
    like($@, qr/Missing arg/            => 'chain set needs arg');

    $val = $obj->set_foo3('bork')->me();
    is($val, 'Foo(1)'                   => 'chain self');

    $val = $obj->set_foo3($b2)->me();
    is($val, 'Foo(1)'                   => 'chain self');


    # get_set - return new
    $obj->bar1('val');
    is($obj->bar1(), 'val'              => 'rvalue set void');

    eval { $obj->bar1(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->bar1($b1);
    is($val, $b1                        => 'rvalue set returns new');

    $val2 = $obj->bar1();
    is($val2, $b1                       => 'rvalue get');

    $obj->bar1() = $b2;
    is($obj->bar1(), $b2                => 'lvalue assign');

    $obj->bar1('foo') = 'Bert';
    is($obj->bar1(), 'Bert'             => 'lvalue assign (arg ignored)');

    $obj->bar1() =~ s/er/re/;
    is($obj->bar1(), 'Bret'             => 'lvalue re');

    change_it($obj->bar1(), 'Fred');
    is($obj->bar1(), 'Fred'             => 'lvalue');
    check_it($obj->bar1(), 'Fred');

    change_it($obj->bar1('Bert'), 'Mike');
    is($obj->bar1(), 'Mike'             => 'lvalue + arg new');
    check_it($obj->bar1('Ralph'), 'Ralph');

    $obj->bar1($b1);
    $val = $obj->bar1()->me();
    is($val, 'Baz(1)'                   => 'chain get');

    $val = $obj->bar1('bork')->me();
    is($val, 'Foo(1)'                   => 'chain self');

    $val = $obj->bar1($b2)->me();
    is($val, 'Baz(2)'                   => 'chain new object');


    # get_set - return old
    $obj->bar2('val');
    is($obj->bar2(), 'val'              => 'rvalue set void');

    eval { $obj->bar2(); };
    is($@, ''                           => 'rvalue get void');

    $obj->bar2($b2);
    $val = $obj->bar2($b1);
    is($val, $b2                        => 'rvalue set returns old');

    $val2 = $obj->bar2();
    is($val2, $b1                       => 'rvalue get');

    $obj->bar2() = $b2;
    is($obj->bar2(), $b2                => 'lvalue assign');

    $obj->bar2('foo') = 'Bert';
    is($obj->bar2(), 'Bert'             => 'lvalue assign (arg ignored)');

    $obj->bar2() =~ s/er/re/;
    is($obj->bar2(), 'Bret'             => 'lvalue re');

    change_it($obj->bar2(), 'Fred');
    is($obj->bar2(), 'Fred'             => 'lvalue');
    check_it($obj->bar2(), 'Fred');

    change_it($obj->bar2('Bert'), 'Mike');
    is($obj->bar2(), 'Bert'             => 'lvalue + arg old');
    check_it($obj->bar2('Ralph'), 'Bert');

    $obj->bar2($b1);
    $val = $obj->bar2()->me();
    is($val, 'Baz(1)'                   => 'chain get');

    $obj->bar2('bork');
    $val = $obj->bar2('bar')->me();
    is($val, 'Foo(1)'                   => 'chain self');

    $obj->bar2($b1);
    $val = $obj->bar2($b2)->me();
    is($val, 'Baz(1)'                   => 'chain old object');


    # get_set - return self
    $obj->bar3('val');
    is($obj->bar3(), 'val'              => 'rvalue set void');

    eval { $obj->bar3(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->bar3($b1);
    is($val, $obj                       => 'rvalue set returns self');

    $val2 = $obj->bar3();
    is($val2, $b1                       => 'rvalue get');

    $obj->bar3() = $b2;
    is($obj->bar3(), $b2                => 'lvalue assign');

    $obj->bar3('foo') = 'Bert';
    is($obj->bar3(), 'Bert'             => 'lvalue assign (arg ignored)');

    $obj->bar3() =~ s/er/re/;
    is($obj->bar3(), 'Bret'             => 'lvalue re');

    change_it($obj->bar3(), 'Fred');
    is($obj->bar3(), 'Fred'              => 'lvalue');
    check_it($obj->bar3(), 'Fred');

    $obj_old = $obj;
    change_it($obj->bar3('Bert'), 'Mike');
    is($obj, 'Mike'                     => 'lvalue + arg self');
    $obj = $obj_old;
    is($obj->bar3(), 'Bert'             => 'Change did set');
    check_it($obj->bar3('Ralph'), $obj);
    is($obj->bar3(), 'Ralph'            => 'Check did set');

    $obj->bar1($b1);
    $val = $obj->bar1()->me();
    is($val, 'Baz(1)'                   => 'chain get');

    $val = $obj->bar3('bork')->me();
    is($val, 'Foo(1)'                   => 'chain self');

    $val = $obj->bar3($b2)->me();
    is($val, 'Foo(1)'                   => 'chain self');


    # get_set - return new - type class
    $obj->baz1($b1);
    is($obj->baz1(), $b1                => 'rvalue set void');

    eval { $obj->baz1('val'); };
    like($@, qr/must be of type 'Baz'/  => 'rvalue set void - bad');

    eval { $obj->baz1(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->baz1($b1);
    is($val, $b1                        => 'rvalue set returns new');

    $val2 = $obj->baz1();
    is($val2, $b1                       => 'rvalue get');

    $obj->baz1() = $b2;
    is($obj->baz1(), $b2                => 'lvalue assign');

    eval { $obj->baz1() = 'val'; };
    like($@, qr/must be of type 'Baz'/  => 'lvalue assign - bad');

    $obj->baz1($obj) = $b2;
    is($obj->baz1(), $b2                => 'lvalue assign (arg ignored)');

    eval { $obj->baz1() =~ s/Baz/Boing/; };     # Evil
    ok(! Scalar::Util::blessed($obj->baz1()) => 'lvalue re');
    like($obj->baz1(), qr/^Boing=SCALAR\(/   => 'lvalue re');

    change_it($obj->baz1(), 'Fred');
    is($obj->baz1(), 'Fred'             => 'lvalue - no type check');
    check_it($obj->baz1(), 'Fred');

    change_it($obj->baz1($b1), 'Mike');
    is($obj->baz1(), 'Mike'             => 'lvalue + arg new - no type check');
    check_it($obj->baz1($b2), $b2);

    $val = $obj->baz1()->me();
    is($val, 'Baz(2)'                   => 'chain get');

    $val = $obj->baz1($b1)->me();
    is($val, 'Baz(1)'                   => 'chain new object');


    # get_set - return old - type class
    $obj->baz2($b2);
    is($obj->baz2(), $b2                => 'rvalue set void');

    eval { $obj->baz2(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->baz2($b1);
    is($val, $b2                        => 'rvalue set returns old');

    $val2 = $obj->baz2();
    is($val2, $b1                       => 'rvalue get');

    $obj->baz2() = $b2;
    is($obj->baz2(), $b2                => 'lvalue assign');

    $obj->baz2($obj) = $b2;
    is($obj->baz2(), $b2                => 'lvalue assign (arg ignored)');

    change_it($obj->baz2(), 'Fred');
    is($obj->baz2(), 'Fred'             => 'lvalue - no type check');
    check_it($obj->baz2(), 'Fred');

    change_it($obj->baz2($b1), 'Mike');
    is($obj->baz2(), $b1                => 'lvalue + arg old');
    check_it($obj->baz2($b2), $b1);

    $val = $obj->baz2()->me();
    is($val, 'Baz(2)'                   => 'chain get');

    $val = $obj->baz2($b1)->me();
    is($val, 'Baz(2)'                   => 'chain old object');


    # get_set - return self - type class
    $obj->baz3($b1);
    is($obj->baz3(), $b1                => 'rvalue set void');

    eval { $obj->baz3(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->baz3($b2);
    is($val, $obj                       => 'rvalue set returns self');

    $val2 = $obj->baz3();
    is($val2, $b2                       => 'rvalue get');

    $obj->baz3() = $b1;
    is($obj->baz3(), $b1                => 'lvalue assign');

    $obj->baz3($obj) = $b2;
    is($obj->baz3(), $b2                => 'lvalue assign (arg ignored)');

    change_it($obj->baz3(), 'Fred');
    is($obj->baz3(), 'Fred'             => 'lvalue - no type check');
    check_it($obj->baz3(), 'Fred');

    $obj_old = $obj;
    change_it($obj->baz3($b1), 'Mike');
    is($obj, 'Mike'                     => 'lvalue + arg self - no type check');
    $obj = $obj_old;
    is($obj->baz3(), $b1                => 'Change did set');
    check_it($obj->baz3($b2), $obj);
    is($obj->baz3(), $b2                => 'Check did set');

    $val = $obj->baz3()->me();
    is($val, 'Baz(2)'                   => 'chain get');

    $val = $obj->baz3($b1)->me();
    is($val, 'Foo(1)'                   => 'chain self');


    # get_set - return new - type num
    $obj->num1(1);
    is($obj->num1(), 1                  => 'rvalue set void');

    eval { $obj->num1($b1); };
    like($@, qr/must be numeric/        => 'rvalue set void - bad');

    eval { $obj->num1(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->num1(2);
    is($val, 2                          => 'rvalue set returns new');

    $val2 = $obj->num1();
    is($val2, 2                         => 'rvalue get');

    $obj->num1() = 3;
    is($obj->num1(), 3                  => 'lvalue assign');

    eval { $obj->num1() = 'val'; };
    like($@, qr/must be numeric/        => 'lvalue assign - bad');

    $obj->num1('bork') = 4;
    is($obj->num1(), 4                  => 'lvalue assign (arg ignored)');

    $obj->num1(5);
    eval { $obj->num1() =~ s/5/Boing/; };     # Evil
    is($obj->num1(), 'Boing'            => 'lvalue re');

    change_it($obj->num1(), 'Fred');
    is($obj->num1(), 'Fred'             => 'lvalue - no type check');
    check_it($obj->num1(), 'Fred');

    change_it($obj->num1(6), 'Mike');
    is($obj->num1(), 'Mike'             => 'lvalue + arg new - no type check');
    check_it($obj->num1(7), 7);

    eval { $val = $obj->num1()->me(); };
    like($@, qr/Can't call method/      => 'chain get needs object');

    $val = $obj->num1(8)->me();
    is($val, 'Foo(1)'                   => 'chain self');
    is($obj->num1(), 8                  => 'chain set');


    # get_set - return old - type num
    $obj->num2(1);
    is($obj->num2(), 1                  => 'rvalue set void');

    eval { $obj->num2(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->num2(2);
    is($val, 1                          => 'rvalue set returns old');

    $val2 = $obj->num2();
    is($val2, 2                         => 'rvalue get');

    $obj->num2() = 3;
    is($obj->num2(), 3                  => 'lvalue assign');

    $obj->num2('bork') = 4;
    is($obj->num2(), 4                  => 'lvalue assign (arg ignored)');

    change_it($obj->num2(), 'Fred');
    is($obj->num2(), 'Fred'             => 'lvalue - no type check');
    check_it($obj->num2(), 'Fred');

    change_it($obj->num2(5), 'Mike');
    is($obj->num2(), 5                  => 'lvalue + arg old');
    check_it($obj->num2(6), 5);

    $val = $obj->num2(7)->me();
    is($val, 'Foo(1)'                   => 'chain self');
    is($obj->num2(), 7                  => 'chain set');


    # get_set - return self - type num
    $obj->num3(1);
    is($obj->num3(), 1                  => 'rvalue set void');

    eval { $obj->num3(); };
    is($@, ''                           => 'rvalue get void');

    $val = $obj->num3(2);
    is($val, $obj                       => 'rvalue set returns self');
    is($obj->num3(), 2                  => 'rvalue set');

    $val2 = $obj->num3();
    is($val2, 2                         => 'rvalue get');

    $obj->num3() = 3;
    is($obj->num3(), 3                  => 'lvalue assign');

    $obj->num3($obj) = 4;
    is($obj->num3(), 4                  => 'lvalue assign (arg ignored)');

    change_it($obj->num3(), 'Fred');
    is($obj->num3(), 'Fred'             => 'lvalue - no type check');
    check_it($obj->num3(), 'Fred');

    $obj_old = $obj;
    change_it($obj->num3(5), 'Mike');
    is($obj, 'Mike'                     => 'lvalue + arg self - no type check');
    $obj = $obj_old;
    is($obj->num3(), 5                  => 'Change did set');
    check_it($obj->num3(6), $obj);
    is($obj->num3(), 6                  => 'Check did set');

    eval { $val = $obj->num3()->me(); };
    like($@, qr/Can't call method/      => 'chain get needs object');

    $val = $obj->num3(7)->me();
    is($val, 'Foo(1)'                   => 'chain self');
    is($obj->num3(), 7                  => 'chain set');

    $obj->bork() = [ {a=>5,b=>'foo'}, {}, {99=>'bork'} ];
    is_deeply($obj->bork(), [ {a=>5,b=>'foo'}, {}, {99=>'bork'} ]
                                        => 'lv array_ref subtype=hash');
}

exit(0);

__END__

:LVALUE                             set    get_set
    $obj->foo('val');                               rvalue set void
    $obj->foo();                    ERR    get      rvalue get void

    my $x = $obj->foo('val');                       rvalue set return
        NEW
        OLD
        SELF
    my $x = $obj->foo();            ERR    get      rvalue get

    $obj->foo() = 'val';                            lvalue assign
    $obj->foo('ignored') = 'val';                   lvalue assign

    $obj->foo() =~ s/x/y/;             fld          lvalue re

    bar($obj->foo());                  fld          lvalue
        change_it
        check_it
    bar($obj->foo('val'));                          lvalue + arg
        change_it
        check_it
        NEW                            fld
        OLD                            ret
        SELF                           obj

    $obj->foo()->bar();             ERR     fld     lvalue + want(obj)
    $obj->foo('val')->bar();                        lvalue + arg + want(obj)
        NEW                            fld/obj
        OLD                            ret/obj
        SELF                           obj

Non-lvalue
    $obj->foo('val');                               rvalue set void
    $obj->foo();                    ERR     get     rvalue get void

    my $x = $obj->foo('val');                       rvalue set return
        NEW
        OLD
        SELF
    my $x = $obj->foo();            ERR     get     rvalue get

    $obj->foo() = 'val';               ERR
    $obj->foo('ignored') = 'val';      ERR
    $obj->foo() =~ s/x/y/;             ERR

    bar($obj->foo());               ERR     get
    bar($obj->foo('val'));
        NEW                            fld
        OLD                            ret
        SELF                           obj

    $obj->foo()->bar();             ERR     get
    $obj->foo('val')->bar();
        NEW                            fld/obj
        OLD                            ret/obj
        SELF                           obj

# EOF
