use strict;
use warnings;

use lib 't/lib';

use Test::More;
use Test::Fatal;

use Moose::Util::MetaRole;


{
    package My::Meta::Class;
    use Moose;
    extends 'Moose::Meta::Class';
}

{
    package Role::Foo;
    use Moose::Role;
    has 'foo' => ( is => 'ro', default => 10 );
}

{
    package My::Class;

    use Moose;
}

{
    package My::Role;
    use Moose::Role;
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => My::Class->meta,
        class_metaroles => { class => ['Role::Foo'] },
    );

    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
        'apply Role::Foo to My::Class->meta()' );
    is( My::Class->meta()->foo(), 10,
        '... and call foo() on that meta object' );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class',
        class_metaroles => { attribute => ['Role::Foo'] },
    );

    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class->meta()'s attribute metaclass} );
    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
        '... My::Class->meta() still does Role::Foo' );

    My::Class->meta()->add_attribute( 'size', is => 'ro' );
    is( My::Class->meta()->get_attribute('size')->foo(), 10,
        '... call foo() on an attribute metaclass object' );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class',
        class_metaroles => { method => ['Role::Foo'] },
    );

    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class->meta()'s method metaclass} );
    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
        '... My::Class->meta() still does Role::Foo' );
    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );

    My::Class->meta()->add_method( 'bar' => sub { 'bar' } );
    is( My::Class->meta()->get_method('bar')->foo(), 10,
        '... call foo() on a method metaclass object' );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class',
        class_metaroles => { wrapped_method => ['Role::Foo'] },
    );

    ok( My::Class->meta()->wrapped_method_metaclass()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class->meta()'s wrapped method metaclass} );
    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
        '... My::Class->meta() still does Role::Foo' );
    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
        '... My::Class->meta() still does Role::Foo' );
    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );

    My::Class->meta()->add_after_method_modifier( 'bar' => sub { 'bar' } );
    is( My::Class->meta()->get_method('bar')->foo(), 10,
        '... call foo() on a wrapped method metaclass object' );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class',
        class_metaroles => { instance => ['Role::Foo'] },
    );

    ok( My::Class->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class->meta()'s instance metaclass} );
    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
        '... My::Class->meta() still does Role::Foo' );
    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );
    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s method metaclass still does Role::Foo} );

    is( My::Class->meta()->get_meta_instance()->foo(), 10,
        '... call foo() on an instance metaclass object' );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class',
        class_metaroles => { constructor => ['Role::Foo'] },
    );

    ok( My::Class->meta()->constructor_class()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class->meta()'s constructor class} );
    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
        '... My::Class->meta() still does Role::Foo' );
    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );
    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s method metaclass still does Role::Foo} );
    ok( My::Class->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s instance metaclass still does Role::Foo} );

    # Actually instantiating the constructor class is too freaking hard!
    ok( My::Class->meta()->constructor_class()->can('foo'),
        '... constructor class has a foo method' );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class',
        class_metaroles => { destructor => ['Role::Foo'] },
    );

    ok( My::Class->meta()->destructor_class()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class->meta()'s destructor class} );
    ok( My::Class->meta()->meta()->does_role('Role::Foo'),
        '... My::Class->meta() still does Role::Foo' );
    ok( My::Class->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s attribute metaclass still does Role::Foo} );
    ok( My::Class->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s method metaclass still does Role::Foo} );
    ok( My::Class->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s instance metaclass still does Role::Foo} );
    ok( My::Class->meta()->constructor_class()->meta()->does_role('Role::Foo'),
        q{... My::Class->meta()'s constructor class still does Role::Foo} );

    # same problem as the constructor class
    ok( My::Class->meta()->destructor_class()->can('foo'),
        '... destructor class has a foo method' );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for            => 'My::Role',
        role_metaroles => { application_to_class => ['Role::Foo'] },
    );

    ok( My::Role->meta->application_to_class_class->meta->does_role('Role::Foo'),
        q{apply Role::Foo to My::Role->meta's application_to_class class} );

    is( My::Role->meta->application_to_class_class->new->foo, 10,
        q{... call foo() on an application_to_class instance} );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for            => 'My::Role',
        role_metaroles => { application_to_role => ['Role::Foo'] },
    );

    ok( My::Role->meta->application_to_role_class->meta->does_role('Role::Foo'),
        q{apply Role::Foo to My::Role->meta's application_to_role class} );
    ok( My::Role->meta->application_to_class_class->meta->does_role('Role::Foo'),
        q{... My::Role->meta's application_to_class class still does Role::Foo} );

    is( My::Role->meta->application_to_role_class->new->foo, 10,
        q{... call foo() on an application_to_role instance} );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for            => 'My::Role',
        role_metaroles => { application_to_instance => ['Role::Foo'] },
    );

    ok( My::Role->meta->application_to_instance_class->meta->does_role('Role::Foo'),
        q{apply Role::Foo to My::Role->meta's application_to_instance class} );
    ok( My::Role->meta->application_to_role_class->meta->does_role('Role::Foo'),
        q{... My::Role->meta's application_to_role class still does Role::Foo} );
    ok( My::Role->meta->application_to_class_class->meta->does_role('Role::Foo'),
        q{... My::Role->meta's application_to_class class still does Role::Foo} );

    is( My::Role->meta->application_to_instance_class->new->foo, 10,
        q{... call foo() on an application_to_instance instance} );
}

{
    Moose::Util::MetaRole::apply_base_class_roles(
        for   => 'My::Class',
        roles => ['Role::Foo'],
    );

    ok( My::Class->meta()->does_role('Role::Foo'),
        'apply Role::Foo to My::Class base class' );
    is( My::Class->new()->foo(), 10,
        '... call foo() on a My::Class object' );
}

{
    package My::Class2;

    use Moose;
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class2',
        class_metaroles => {
            class       => ['Role::Foo'],
            attribute   => ['Role::Foo'],
            method      => ['Role::Foo'],
            instance    => ['Role::Foo'],
            constructor => ['Role::Foo'],
            destructor  => ['Role::Foo'],
        },
    );

    ok( My::Class2->meta()->meta()->does_role('Role::Foo'),
        'apply Role::Foo to My::Class2->meta()' );
    is( My::Class2->meta()->foo(), 10,
        '... and call foo() on that meta object' );
    ok( My::Class2->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class2->meta()'s attribute metaclass} );
    My::Class2->meta()->add_attribute( 'size', is => 'ro' );

    is( My::Class2->meta()->get_attribute('size')->foo(), 10,
        '... call foo() on an attribute metaclass object' );

    ok( My::Class2->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class2->meta()'s method metaclass} );

    My::Class2->meta()->add_method( 'bar' => sub { 'bar' } );
    is( My::Class2->meta()->get_method('bar')->foo(), 10,
        '... call foo() on a method metaclass object' );

    ok( My::Class2->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class2->meta()'s instance metaclass} );
    is( My::Class2->meta()->get_meta_instance()->foo(), 10,
        '... call foo() on an instance metaclass object' );

    ok( My::Class2->meta()->constructor_class()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class2->meta()'s constructor class} );
    ok( My::Class2->meta()->constructor_class()->can('foo'),
        '... constructor class has a foo method' );

    ok( My::Class2->meta()->destructor_class()->meta()->does_role('Role::Foo'),
        q{apply Role::Foo to My::Class2->meta()'s destructor class} );
    ok( My::Class2->meta()->destructor_class()->can('foo'),
        '... destructor class has a foo method' );
}


{
    package My::Meta;

    use Moose::Exporter;
    Moose::Exporter->setup_import_methods( also => 'Moose' );

    sub init_meta {
        shift;
        my %p = @_;

        Moose->init_meta( %p, metaclass => 'My::Meta::Class' );
    }
}

{
    package My::Class3;

    My::Meta->import();
}


{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class3',
        class_metaroles => { class => ['Role::Foo'] },
    );

    ok( My::Class3->meta()->meta()->does_role('Role::Foo'),
        'apply Role::Foo to My::Class3->meta()' );
    is( My::Class3->meta()->foo(), 10,
        '... and call foo() on that meta object' );
    ok( ( grep { $_ eq 'My::Meta::Class' } My::Class3->meta()->meta()->superclasses() ),
        'apply_metaroles() does not interfere with metaclass set via Moose->init_meta()' );
}

{
    package Role::Bar;
    use Moose::Role;
    has 'bar' => ( is => 'ro', default => 200 );
}

{
    package My::Class4;
    use Moose;
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class4',
        class_metaroles => { class => ['Role::Foo'] },
    );

    ok( My::Class4->meta()->meta()->does_role('Role::Foo'),
        'apply Role::Foo to My::Class4->meta()' );

    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class4',
        class_metaroles => { class => ['Role::Bar'] },
    );

    ok( My::Class4->meta()->meta()->does_role('Role::Bar'),
        'apply Role::Bar to My::Class4->meta()' );
    ok( My::Class4->meta()->meta()->does_role('Role::Foo'),
        '... and My::Class4->meta() still does Role::Foo' );
}

{
    package My::Class5;
    use Moose;

    extends 'My::Class';
}

{
    ok( My::Class5->meta()->meta()->does_role('Role::Foo'),
        q{My::Class5->meta()'s does Role::Foo because it extends My::Class} );
    ok( My::Class5->meta()->attribute_metaclass()->meta()->does_role('Role::Foo'),
        q{My::Class5->meta()'s attribute metaclass also does Role::Foo} );
    ok( My::Class5->meta()->method_metaclass()->meta()->does_role('Role::Foo'),
        q{My::Class5->meta()'s method metaclass also does Role::Foo} );
    ok( My::Class5->meta()->instance_metaclass()->meta()->does_role('Role::Foo'),
        q{My::Class5->meta()'s instance metaclass also does Role::Foo} );
    ok( My::Class5->meta()->constructor_class()->meta()->does_role('Role::Foo'),
        q{My::Class5->meta()'s constructor class also does Role::Foo} );
    ok( My::Class5->meta()->destructor_class()->meta()->does_role('Role::Foo'),
        q{My::Class5->meta()'s destructor class also does Role::Foo} );
}

{
    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class5',
        class_metaroles => { class => ['Role::Bar'] },
    );

    ok( My::Class5->meta()->meta()->does_role('Role::Bar'),
        q{apply Role::Bar My::Class5->meta()} );
    ok( My::Class5->meta()->meta()->does_role('Role::Foo'),
        q{... and My::Class5->meta() still does Role::Foo} );
}

{
    package My::Class6;
    use Moose;

    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class6',
        class_metaroles => { class => ['Role::Bar'] },
    );

    extends 'My::Class';
}

{
    ok( My::Class6->meta()->meta()->does_role('Role::Bar'),
        q{apply Role::Bar My::Class6->meta() before extends} );
    ok( My::Class6->meta()->meta()->does_role('Role::Foo'),
        q{... and My::Class6->meta() does Role::Foo because My::Class6 extends My::Class} );
}

# This is the hack that used to be needed to work around the
# _fix_metaclass_incompatibility problem. You called extends() (which
# in turn calls _fix_metaclass_imcompatibility) _before_ you apply
# more extensions in the subclass. We wabt to make sure this continues
# to work in the future.
{
    package My::Class7;
    use Moose;

    # In real usage this would go in a BEGIN block so it happened
    # before apply_metaroles was called by an extension.
    extends 'My::Class';

    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class7',
        class_metaroles => { class => ['Role::Bar'] },
    );
}

{
    ok( My::Class7->meta()->meta()->does_role('Role::Bar'),
        q{apply Role::Bar My::Class7->meta() before extends} );
    ok( My::Class7->meta()->meta()->does_role('Role::Foo'),
        q{... and My::Class7->meta() does Role::Foo because My::Class7 extends My::Class} );
}

{
    package My::Class8;
    use Moose;

    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class8',
        class_metaroles => {
            class     => ['Role::Bar'],
            attribute => ['Role::Bar'],
        },
    );

    extends 'My::Class';
}

{
    ok( My::Class8->meta()->meta()->does_role('Role::Bar'),
        q{apply Role::Bar My::Class8->meta() before extends} );
    ok( My::Class8->meta()->meta()->does_role('Role::Foo'),
        q{... and My::Class8->meta() does Role::Foo because My::Class8 extends My::Class} );
    ok( My::Class8->meta()->attribute_metaclass->meta()->does_role('Role::Bar'),
        q{apply Role::Bar to My::Class8->meta()->attribute_metaclass before extends} );
    ok( My::Class8->meta()->attribute_metaclass->meta()->does_role('Role::Foo'),
        q{... and My::Class8->meta()->attribute_metaclass does Role::Foo because My::Class8 extends My::Class} );
}


{
    package My::Class9;
    use Moose;

    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class9',
        class_metaroles => { attribute => ['Role::Bar'] },
    );

    extends 'My::Class';
}

{
    ok( My::Class9->meta()->meta()->does_role('Role::Foo'),
        q{... and My::Class9->meta() does Role::Foo because My::Class9 extends My::Class} );
    ok( My::Class9->meta()->attribute_metaclass->meta()->does_role('Role::Bar'),
        q{apply Role::Bar to My::Class9->meta()->attribute_metaclass before extends} );
    ok( My::Class9->meta()->attribute_metaclass->meta()->does_role('Role::Foo'),
        q{... and My::Class9->meta()->attribute_metaclass does Role::Foo because My::Class9 extends My::Class} );
}

# This tests applying meta roles to a metaclass's metaclass. This is
# completely insane, but is exactly what happens with
# Fey::Meta::Class::Table. It's a subclass of Moose::Meta::Class
# itself, and then it _uses_ MooseX::ClassAttribute, so the metaclass
# for Fey::Meta::Class::Table does a role.
#
# At one point this caused a metaclass incompatibility error down
# below, when we applied roles to the metaclass of My::Class10. It's
# all madness but as long as the tests pass we're happy.
{
    package My::Meta::Class2;
    use Moose;
    extends 'Moose::Meta::Class';

    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Meta::Class2',
        class_metaroles => { class => ['Role::Foo'] },
    );
}

{
    package My::Object;
    use Moose;
    extends 'Moose::Object';
}

{
    package My::Meta2;

    use Moose::Exporter;
    Moose::Exporter->setup_import_methods( also => 'Moose' );

    sub init_meta {
        shift;
        my %p = @_;

        Moose->init_meta(
            %p,
            metaclass  => 'My::Meta::Class2',
            base_class => 'My::Object',
        );
    }
}

{
    package My::Class10;
    My::Meta2->import;

    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class10',
        class_metaroles => { class => ['Role::Bar'] },
    );
}

{
    ok( My::Class10->meta()->meta()->meta()->does_role('Role::Foo'),
        q{My::Class10->meta()->meta() does Role::Foo } );
    ok( My::Class10->meta()->meta()->does_role('Role::Bar'),
        q{My::Class10->meta()->meta() does Role::Bar } );
    ok( My::Class10->meta()->isa('My::Meta::Class2'),
        q{... and My::Class10->meta still isa(My::Meta::Class2)} );
    ok( My::Class10->isa('My::Object'),
        q{... and My::Class10 still isa(My::Object)} );
}

{
    package My::Constructor;

    use parent 'Moose::Meta::Method::Constructor';
}

{
    package My::Class11;

    use Moose;

    __PACKAGE__->meta->constructor_class('My::Constructor');

    Moose::Util::MetaRole::apply_metaroles(
        for             => 'My::Class11',
        class_metaroles => { class => ['Role::Foo'] },
    );
}

{
    ok( My::Class11->meta()->meta()->does_role('Role::Foo'),
        q{My::Class11->meta()->meta() does Role::Foo } );
    is( My::Class11->meta()->constructor_class, 'My::Constructor',
        q{... and explicitly set constructor_class value is unchanged)} );
}

{
    package ExportsMoose;

    Moose::Exporter->setup_import_methods(
        also => 'Moose',
    );

    sub init_meta {
        shift;
        my %p = @_;
        Moose->init_meta(%p);
        return Moose::Util::MetaRole::apply_metaroles(
            for => $p{for_class},
            # Causes us to recurse through init_meta, as we have to
            # load MyMetaclassRole from disk.
            class_metaroles => { class => [qw/MyMetaclassRole/] },
        );
    }
}

is( exception {
    package UsesExportedMoose;
    ExportsMoose->import;
}, undef, 'import module which loads a role from disk during init_meta' );

{
    package Foo::Meta::Role;

    use Moose::Role;
}

{
    package Foo::Role;

    Moose::Exporter->setup_import_methods(
        also => 'Moose::Role',
    );

    sub init_meta {
        shift;
        my %p = @_;

        Moose::Role->init_meta(%p);

        return Moose::Util::MetaRole::apply_metaroles(
            for            => $p{for_class},
            role_metaroles => { method => ['Foo::Meta::Role'] },
        );
    }
}

{
    package Role::Baz;

    Foo::Role->import;

    sub bla {}
}

{
    package My::Class12;

    use Moose;

    with( 'Role::Baz' );
}

{
    ok(
        My::Class12->meta->does_role( 'Role::Baz' ),
        'role applied'
    );

    my $method = My::Class12->meta->get_method( 'bla' );
    ok(
        $method->meta->does_role( 'Foo::Meta::Role' ),
        'method_metaclass_role applied'
    );
}

{
    package Parent;
    use Moose;

    Moose::Util::MetaRole::apply_metaroles(
        for             => __PACKAGE__,
        class_metaroles => { constructor => ['Role::Foo'] },
    );
}

{
    package Child;

    use Moose;
    extends 'Parent';
}

{
    ok(
        Parent->meta->constructor_class->meta->can('does_role')
            && Parent->meta->constructor_class->meta->does_role('Role::Foo'),
        'Parent constructor class has metarole from Parent'
    );

    ok(
        Child->meta->constructor_class->meta->can('does_role')
            && Child->meta->constructor_class->meta->does_role(
            'Role::Foo'),
        'Child constructor class has metarole from Parent'
    );
}

{
    package NotMoosey;

    use metaclass;
}

{
    like(
        exception {
            Moose::Util::MetaRole::apply_metaroles(
                for             => 'Does::Not::Exist',
                class_metaroles => { class => ['Role::Foo'] },
            );
        },
        qr/When using Moose::Util::MetaRole.+You passed Does::Not::Exist.+Maybe you need to call.+/,
        'useful error when apply metaroles to a class without a metaclass'
    );

    like(
        exception {
            Moose::Util::MetaRole::apply_metaroles(
                for             => 'NotMoosey',
                class_metaroles => { class => ['Role::Foo'] },
            );
        },
        qr/When using Moose::Util::MetaRole.+You passed NotMoosey.+we resolved this to a Class::MOP::Class object.+/,
        'useful error when using apply metaroles to a class with a Class::MOP::Class metaclass'
    );

    like(
        exception {
            Moose::Util::MetaRole::apply_base_class_roles(
                for   => 'NotMoosey',
                roles => { class => ['Role::Foo'] },
            );
        },
        qr/When using Moose::Util::MetaRole.+You passed NotMoosey.+we resolved this to a Class::MOP::Class object.+/,
        'useful error when applying base class to roles to a non-Moose class'
    );

    like(
        exception {
            Moose::Util::MetaRole::apply_base_class_roles(
                for   => 'My::Role',
                roles => { class => ['Role::Foo'] },
            );
        },
        qr/You can only apply base class roles to a Moose class.+/,
        'useful error when applying base class to roles to a non-Moose class'
    );
}

done_testing;
