use strict;
use warnings;

use Test::Needs 'Test::Output';  # skip all if not installed
use Test::More;
use Test::Output;

use Class::MOP;

{
    package HasConstructor;

    sub new { bless {}, $_[0] }

    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    $meta->superclasses('NotMoose');

    ::stderr_like(
        sub { $meta->make_immutable },
        qr/\QNot inlining a constructor for HasConstructor since it defines its own constructor.\E\s+\QIf you are certain you don't need to inline your constructor, specify inline_constructor => 0 in your call to HasConstructor->meta->make_immutable\E/,
        'got a warning that Foo will not have an inlined constructor because it defines its own new method'
    );

    ::is(
        $meta->find_method_by_name('new')->body,
        HasConstructor->can('new'),
        'HasConstructor->new was untouched'
    );
}

{
    package My::Constructor;

    use parent 'Class::MOP::Method::Constructor';

    sub _expected_method_class { 'Base::Class' }
}

{
    package No::Constructor;
}

{
    package My::Constructor2;

    use parent 'Class::MOP::Method::Constructor';

    sub _expected_method_class { 'No::Constructor' }
}

{
    package Base::Class;

    sub new { bless {}, $_[0] }
    sub DESTROY { }
}

{
    package NotMoose;

    sub new {
        my $class = shift;

        return bless { not_moose => 1 }, $class;
    }
}

{
    package Foo;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    $meta->superclasses('NotMoose');

    ::stderr_like(
        sub { $meta->make_immutable( constructor_class => 'My::Constructor' ) },
        qr/\QNot inlining 'new' for Foo since it is not inheriting the default Base::Class::new\E\s+\QIf you are certain you don't need to inline your constructor, specify inline_constructor => 0 in your call to Foo->meta->make_immutable/,
        'got a warning that Foo will not have an inlined constructor'
    );

    ::is(
        $meta->find_method_by_name('new')->body,
        NotMoose->can('new'),
        'Foo->new is inherited from NotMoose'
    );
}

{
    package Bar;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    $meta->superclasses('NotMoose');

    ::stderr_is(
        sub { $meta->make_immutable( replace_constructor => 1 ) },
        q{},
        'no warning when replace_constructor is true'
    );

    ::is(
        $meta->find_method_by_name('new')->package_name,
        'Bar',
        'Bar->new is inlined, and not inherited from NotMoose'
    );
}

{
    package Baz;
    Class::MOP::Class->initialize(__PACKAGE__)->make_immutable;
}

{
    package Quux;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    $meta->superclasses('Baz');

    ::stderr_is(
        sub { $meta->make_immutable },
        q{},
        'no warning when inheriting from a class that has already made itself immutable'
    );
}

{
    package Whatever;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    ::stderr_like(
        sub { $meta->make_immutable( constructor_class => 'My::Constructor2' ) },
        qr/\QNot inlining 'new' for Whatever since No::Constructor::new is not defined/,
        'got a warning that Whatever will not have an inlined constructor because its expected inherited method does not exist'
    );
}

{
    package My::Constructor3;

    use parent 'Class::MOP::Method::Constructor';
}

{
    package CustomCons;

    Class::MOP::Class->initialize(__PACKAGE__)->make_immutable( constructor_class => 'My::Constructor3' );
}

{
    package Subclass;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    $meta->superclasses('CustomCons');

    ::stderr_is(
        sub { $meta->make_immutable },
        q{},
        'no warning when inheriting from a class that has already made itself immutable'
    );
}

{
    package ModdedNew;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    sub new { bless {}, shift }

    $meta->add_before_method_modifier( 'new' => sub { } );
}

{
    package ModdedSub;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    $meta->superclasses('ModdedNew');

    ::stderr_like(
        sub { $meta->make_immutable },
        qr/\QNot inlining 'new' for ModdedSub since it has method modifiers which would be lost if it were inlined/,
        'got a warning that ModdedSub will not have an inlined constructor since it inherited a wrapped new'
    );
}

{
    package My::Destructor;

    use parent 'Class::MOP::Method::Inlined';

    sub new {
        my $class   = shift;
        my %options = @_;

        my $self = bless \%options, $class;
        $self->_inline_destructor;

        return $self;
    }

    sub _inline_destructor {
        my $self = shift;

        my $code = $self->_compile_code('sub { }');

        $self->{body} = $code;
    }

    sub is_needed { 1 }
    sub associated_metaclass { $_[0]->{metaclass} }
    sub body { $_[0]->{body} }
    sub _expected_method_class { 'Base::Class' }
}

{
    package HasDestructor;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    sub DESTROY { }

    ::stderr_like(
        sub {
            $meta->make_immutable(
                inline_destructor => 1,
                destructor_class  => 'My::Destructor',
            );
        },
        qr/Not inlining a destructor for HasDestructor since it defines its own destructor./,
        'got a warning when trying to inline a destructor for a class that already defines DESTROY'
    );

    ::is(
        $meta->find_method_by_name('DESTROY')->body,
        HasDestructor->can('DESTROY'),
        'HasDestructor->DESTROY was untouched'
    );
}

{
    package HasDestructor2;
    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    sub DESTROY { }

    $meta->make_immutable(
        inline_destructor  => 1,
        destructor_class   => 'My::Destructor',
        replace_destructor => 1
    );

    ::stderr_is(
        sub {
            $meta->make_immutable(
                inline_destructor  => 1,
                destructor_class   => 'My::Destructor',
                replace_destructor => 1
            );
        },
        q{},
        'no warning when replace_destructor is true'
    );

    ::isnt(
        $meta->find_method_by_name('new')->body,
        HasConstructor2->can('new'),
        'HasConstructor2->new was replaced'
    );
}

{
    package ParentHasDestructor;

    sub DESTROY { }
}

{
    package DestructorChild;

    use parent -norequire => 'ParentHasDestructor';

    my $meta = Class::MOP::Class->initialize(__PACKAGE__);

    ::stderr_like(
        sub {
            $meta->make_immutable(
                inline_destructor => 1,
                destructor_class  => 'My::Destructor',
            );
        },
        qr/Not inlining 'DESTROY' for DestructorChild since it is not inheriting the default Base::Class::DESTROY/,
        'got a warning when trying to inline a destructor in a class that inherits an unexpected DESTROY'
    );
}

done_testing;
