The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use strict;
use warnings;

use Test::More 0.88;

use lib 't/lib';

use Fey::ORM::Test;
use Fey::Literal::String;
use Fey::Placeholder;
use Fey::Test;

Fey::ORM::Test::define_live_classes();
Fey::ORM::Test::insert_user_data();

{
    package User;

    use Fey::ORM::Table;

    has_many messages => ( table => Schema->Schema()->table('Message') );

    package Message;

    use Fey::ORM::Table;

    has_many 'child_messages' =>
        ( table => Schema->Schema()->table('Message') );
}

{
    my $parent = Message->insert(
        message_id => 1,
        message    => 'parent body',
        user_id    => 1,
    );

    for my $i ( 1 .. 3 ) {
        Message->insert(
            message_id        => $i * 3,
            message           => 'child body',
            parent_message_id => $parent->message_id(),
            user_id           => ( $i % 2 ? 1 : 42 ),
        );
    }

    my $user = User->new( user_id => 1 );

    my $messages = $user->messages();

    is_deeply(
        [ sort map { $_->message_id() } $messages->all() ],
        [ 1, 3, 9 ],
        'messages() method returns iterator with expected message data'
    );

    $messages = $parent->child_messages();

    is_deeply(
        [ sort map { $_->message_id() } $messages->all() ],
        [ 3, 6, 9 ],
        'child_messages() method returns iterator with expected message data'
    );
}

{
    package User;

    __PACKAGE__->meta()->remove_has_many('message');

    has_many messages => (
        table    => Schema->Schema()->table('Message'),
        order_by => [
            Schema->Schema()->table('Message')->column('message_id'), 'DESC'
        ],
    );
}

{
    my $user = User->new( user_id => 1 );

    my $messages = $user->messages();

    is_deeply(
        [ map { $_->message_id() } $messages->all() ],
        [ 9, 3, 1 ],
        'messages() method returns iterator with expected message data, respecting order_by'
    );
}

{
    package User;

    __PACKAGE__->meta()->remove_has_many('message');

    has_many messages => (
        table    => Schema->Schema()->table('Message'),
        cache    => 1,
        order_by => [
            Schema->Schema()->table('Message')->column('message_id'), 'ASC'
        ],
    );
}

{
    my $user = User->new( user_id => 1 );

    isa_ok( $user->messages(), 'Fey::Object::Iterator::FromSelect::Caching' );

    my $m1 = $user->messages();
    my $m2 = $user->messages();

    isnt(
        $m1, $m2,
        'cached has_many methods return a new iterator on each call'
    );
    is(
        $m1->next()->message_id(), 1,
        'first iterator ->next returns first message'
    );
    is(
        $m2->next()->message_id(), 1,
        'second iterator ->next returns first message (they do not share state)'
    );
    is(
        $m1->next()->message_id(), 3,
        'first iterator ->next returns second message'
    );
    is(
        $m1->next()->message_id(), 9,
        'first iterator ->next returns third message'
    );
    is(
        $m2->next()->message_id(), 3,
        'second iterator ->next returns second message (they do not share state)'
    );

    my $m3 = $user->messages();

    my @nested;
    while ( my $message = $m3->next() ) {
        my $m4 = $user->messages();

        push @nested,
            [
            $message->message_id(),
            map { $_->message_id() } $m4->all()
            ];
    }

    is_deeply(
        \@nested,
        [
            [ 1, 1, 3, 9 ],
            [ 3, 1, 3, 9 ],
            [ 9, 1, 3, 9 ],
        ],
        'Can access a cloned iterator while looping over the original'
    );
}

{
    my $schema = Schema->Schema();

    $schema->remove_foreign_key($_)
        for $schema->foreign_keys_between_tables( 'Message', 'User' );

    $schema->remove_foreign_key($_)
        for $schema->foreign_keys_between_tables( 'Message', 'Message' );

    # These definitions invert the source/target labeling of the
    # corresponding FKs in Fey::Test. The goal is to test that
    # has_many figures out the proper "direction" of the FK.
    my $fk1 = Fey::FK->new(
        source_columns => [ $schema->table('Message')->column('user_id') ],
        target_columns => [ $schema->table('User')->column('user_id') ],
    );

    my $fk2 = Fey::FK->new(
        source_columns =>
            [ $schema->table('Message')->column('parent_message_id') ],
        target_columns => [ $schema->table('Message')->column('message_id') ],
    );

    $schema->add_foreign_key($_) for $fk1, $fk2;

    package User;

    __PACKAGE__->meta()->remove_has_many('messages');

    has_many messages => ( table => Schema->Schema()->table('Message') );

    package Message;

    __PACKAGE__->meta()->remove_has_many('child_messages');

    has_many 'child_messages' =>
        ( table => Schema->Schema()->table('Message') );
}

inverted_fk_tests();

{
    # This next set of tests is the same as the last, except this time we
    # explicitly provide the Fey::FK object, and test that it gets inverted.

    my $schema = Schema->Schema();

    package User;

    __PACKAGE__->meta()->remove_has_many('messages');

    my ($fk)
        = $schema->foreign_keys_between_tables(
        $schema->tables( 'Message', 'User' ) );

    has_many messages => (
        table => Schema->Schema()->table('Message'),
        fk    => $fk,
    );

    package Message;

    __PACKAGE__->meta()->remove_has_many('child_messages');

    ($fk)
        = $schema->foreign_keys_between_tables(
        $schema->tables( 'Message', 'Message' ) );

    has_many 'child_messages' => (
        table => Schema->Schema()->table('Message'),
        fk    => $fk,
    );
}

inverted_fk_tests();

{
    package Message;

    my $schema = Schema->Schema();

    my $select
        = Schema->SQLFactoryClass()->new_select()
        ->select( $schema->table('User') )
        ->from( $schema->table('User'), $schema->table('Message') )->where(
        $schema->table('Message')->column('parent_message_id'),
        '=', Fey::Placeholder->new()
        )->order_by( $schema->table('Message')->column('message_id') );

    has_many 'child_message_users' => (
        table       => $schema->table('User'),
        select      => $select,
        bind_params => sub { $_[0]->message_id() },
    );
}

{
    my $message = Message->new( message_id => 1 );

    my @users = $message->child_message_users()->all();

    is(
        scalar @users, 3,
        'found two users from child_message_user()'
    );

    is_deeply(
        [ map { $_->user_id() } @users ],
        [ 1, 42, 1 ],
        'users are returned in expected order'
    );

    my $message2 = Message->new( message_id => 1 );
    isnt(
        $message->child_message_users(),
        $message2->child_message_users(),
        'two objects do not share a single iterator'
    );
}

sub inverted_fk_tests {
    local $Test::Builder::Level = $Test::Builder::Level + 1;

    {
        my $user = User->new( user_id => 1 );

        my $messages = $user->messages();

        is_deeply(
            [ sort map { $_->message_id() } $messages->all() ],
            [ 1, 3, 9 ],
            'messages() method returns iterator with expected message data'
        );

        my $parent = Message->new( message_id => 1 );

        $messages = $parent->child_messages();

        is_deeply(
            [ sort map { $_->message_id() } $messages->all() ],
            [ 3, 6, 9 ],
            'messages() method returns iterator with expected message data'
        );
    }

    {
        my $user = User->new( user_id => 1 );

        my $messages = $user->messages();

        $messages->next();
        $messages->next();

        $messages = $user->messages();

        is_deeply(
            [ sort map { $_->message_id() } $messages->all() ],
            [ 1, 3, 9 ],
            'messages() method resets iterator with each call'
        );
    }
}

done_testing();