The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
## no critic (Moose::RequireMakeImmutable)
package Fey::ORM::Test::Iterator;

use strict;
use warnings;

use Fey::SQL;

use Fey::Object::Iterator::FromSelect;
use Fey::ORM::Test;
use Test::Fatal;
use Test::More 0.88;

Fey::ORM::Test::require_sqlite();

sub run_shared_tests {
    my $class = shift;

    local $Test::Builder::Level = $Test::Builder::Level + 1;

    Fey::ORM::Test::insert_user_data();
    Fey::ORM::Test::insert_message_data();
    Fey::ORM::Test::define_basic_classes();

    my $schema = Fey::ORM::Test->schema();
    my $dbh    = Fey::Test::SQLite->dbh();

    {
        my $sql
            = Fey::SQL->new_select->select(
            $schema->table('User')->columns(qw( user_id username email )) )
            ->from( $schema->table('User') )
            ->order_by( $schema->table('User')->column('user_id') );

        my $iterator = _make_iterator(
            $class,
            classes => 'User',
            dbh     => $dbh,
            select  => $sql,
        );

        is(
            $iterator->index(), 0,
            'index() is 0 before any data has been fetched'
        );

        my $user = $iterator->next();
        isa_ok( $user, 'User' );

        is(
            $iterator->index(), 1,
            'index() is 1 after first row has been fetched'
        );

        is(
            $user->user_id(), 1,
            'user_id = 1'
        );
        is(
            $user->username(), 'autarch',
            'username = autarch'
        );
        is(
            $user->email(), 'autarch@example.com',
            'email = autarch@example.com'
        );

        $user = $iterator->next();

        is(
            $iterator->index(), 2,
            'index() is 2 after second row has been fetched'
        );

        is(
            $user->user_id(), 42,
            'user_id = 42'
        );
        is(
            $user->username(), 'bubba',
            'username = bubba'
        );
        is(
            $user->email(), 'bubba@example.com',
            'email = bubba@example.com'
        );

        $user = $iterator->next();

        is(
            $iterator->index(), 2,
            'index() is 2 after attempt to fetch another row'
        );
        is(
            $user, undef,
            '$user is undef when there are no more objects to fetch'
        );

        $iterator->reset();

        $user = $iterator->next();

        is(
            $iterator->index(), 1,
            'index() is 1 after reset and first row has been fetched'
        );

        is(
            $user->user_id(), 1,
            'user_id = 1'
        );
        is(
            $user->username(), 'autarch',
            'username = autarch'
        );
        is(
            $user->email(), 'autarch@example.com',
            'email = autarch@example.com'
        );
    }

    {
        my $sql
            = Fey::SQL->new_select->select(
            $schema->table('User')->columns(qw( user_id username email )) )
            ->from( $schema->table('User') )
            ->order_by( $schema->table('User')->column('user_id') );

        my $iterator = _make_iterator(
            $class,
            classes => 'User',
            dbh     => $dbh,
            select  => $sql,
        );

        # Makes sure that ->all resets first
        $iterator->next();

        my @users = $iterator->all();

        is_deeply(
            [ sort map { $_->user_id() } @users ],
            [ 1, 42 ],
            'all() returns expected result'
        );

        $iterator->reset();

        my %user = $iterator->next_as_hash();

        is(
            ( scalar keys %user ), 1,
            'next_as_hash() returns hash with one key'
        );
        is(
            $user{User}->user_id(), 1,
            'found expected user via next_as_hash()'
        );

        # Makes sure that ->all resets first
        $iterator->next();

        my @results = $iterator->all_as_hashes();

        is_deeply(
            [ map { [ keys %{$_} ] } @results ],
            [ ['User'], ['User'] ],
            'all_as_hashes returns arrayref of hashes with expected keys'
        );

        is(
            $results[0]{User}->user_id(), 1,
            'found expected first user in result'
        );
        is(
            $results[1]{User}->user_id(), 42,
            'found expected second user in result'
        );

        $iterator->reset();
        $iterator->next();

        @users = $iterator->remaining();

        is_deeply(
            [ sort map { $_->user_id() } @users ],
            [42],
            'remaining() returns expected result'
        );

        $iterator->reset();
        $iterator->next();

        @results = $iterator->remaining_as_hashes();

        is_deeply(
            [ map { [ keys %{$_} ] } @results ],
            [ ['User'] ],
            'remaining_as_hashes returns arrayref of hashes with expected keys'
        );
    }

    {
        my $sql
            = Fey::SQL->new_select->select(
            $schema->table('User')->columns(qw( user_id username email )) )
            ->from( $schema->table('User') )
            ->where( $schema->table('User')->column('user_id'), 'IN', 1, 42 )
            ->order_by( $schema->table('User')->column('user_id') );

        my $iterator = _make_iterator(
            $class,
            classes => 'User',
            dbh     => $dbh,
            select  => $sql,
        );

        my $user = $iterator->next();

        is(
            $user->user_id(), 1,
            'first user_id with bind params in sql object'
        );

        $user = $iterator->next();

        is(
            $user->user_id(), 42,
            'second user_id with bind params in sql object'
        );
    }

    {
        my $sql
            = Fey::SQL->new_select->select(
            $schema->table('User')->columns(qw( user_id username email )) )
            ->from( $schema->table('User') )->where(
            $schema->table('User')->column('user_id'), 'IN',
            ( Fey::Placeholder->new() ) x 2
            )->order_by( $schema->table('User')->column('user_id') );

        my $iterator = _make_iterator(
            $class,
            classes     => 'User',
            dbh         => $dbh,
            select      => $sql,
            bind_params => [ 1, 42 ],
        );

        my $user = $iterator->next();

        is(
            $user->user_id(), 1,
            'first user_id with explicit bind params'
        );

        $user = $iterator->next();

        is(
            $user->user_id(), 42,
            'second user_id with explicit bind params'
        );
    }

    {
        my $sql = Fey::SQL->new_select->select(
            $schema->table('User')->columns(qw( user_id username )),
            $schema->table('Message')->columns(qw( message_id message )),
            )->from( $schema->tables( 'User', 'Message' ) )->order_by(
            $schema->table('User')->column('user_id'),
            $schema->table('Message')->column('message_id'),
            );

        my $iterator = _make_iterator(
            $class,
            classes => [ 'User', 'Message' ],
            dbh     => $dbh,
            select  => $sql,
        );

        my ( $user, $message ) = $iterator->next();

        is( $user->user_id(),       1, 'first user id is 1' );
        is( $message->message_id(), 1, 'first message id is 1' );

        $user = $iterator->next();

        # testing next() in scalar context
        isa_ok( $user, 'User' );

        $iterator->reset();

        is_deeply(
            [
                map { [ $_->[0]->user_id(), $_->[1]->message_id() ] }
                    $iterator->all()
            ],
            [
                [ 1,  1 ],
                [ 1,  2 ],
                [ 42, 10 ],
                [ 42, 99 ],
            ],
            'all() returns expected set of objects'
        );

        $iterator->reset();

        my %result = $iterator->next_as_hash();

        is( $result{User}->user_id(),       1, 'first user id is 1' );
        is( $result{Message}->message_id(), 1, 'first message id is 1' );

        $iterator->reset();

        is_deeply(
            [
                map { [ $_->{User}->user_id(), $_->{Message}->message_id() ] }
                    $iterator->all_as_hashes()
            ],
            [
                [ 1,  1 ],
                [ 1,  2 ],
                [ 42, 10 ],
                [ 42, 99 ],
            ],
            'all_as_hashes() returns expected set of objects'
        );
    }

    {

        # This simulates an OUTER JOIN where Message could be NULL
        my $sql = Fey::SQL->new_select->select(
            Fey::Literal::Null->new(),
            $schema->table('User')->columns(qw( user_id username )),
            )->from( $schema->tables('User') )
            ->order_by( $schema->table('User')->column('user_id') );

        my $iterator = _make_iterator(
            $class,
            classes       => [ 'Message', 'User' ],
            dbh           => $dbh,
            select        => $sql,
            attribute_map => {
                0 => {
                    class     => 'Message',
                    attribute => 'message_id',
                },
                1 => {
                    class     => 'User',
                    attribute => 'user_id',
                },
                2 => {
                    class     => 'User',
                    attribute => 'username',
                },
            },
        );

        my ( $message, $user ) = $iterator->next();

        is( $message, undef, 'message object is undefined' );
        ok( $user, 'user object is defined' );
    }

    {
        my $sql
            = Fey::SQL->new_select->select(
            $schema->table('User')->columns(qw( user_id username email )) )
            ->from( $schema->table('User') )
            ->order_by( $schema->table('User')->column('user_id') );

        my $iterator = _make_iterator(
            $class,
            classes       => 'FakeUser',
            dbh           => $dbh,
            select        => $sql,
            attribute_map => {
                0 => {
                    class     => 'FakeUser',
                    attribute => 'user_id',
                },
                1 => {
                    class     => 'FakeUser',
                    attribute => 'username',
                },
                2 => {
                    class     => 'FakeUser',
                    attribute => 'email',
                },
            },
        );

        my $user = $iterator->next();
        isa_ok( $user, 'FakeUser' );

        is(
            $user->user_id(), 1,
            'user_id = 1'
        );
        is(
            $user->username(), 'autarch',
            'username = autarch'
        );
        is(
            $user->email(), 'autarch@example.com',
            'email = autarch@example.com'
        );

        like(
            exception { $iterator->next_as_hash() },
            qr/\QCannot make a hash unless all classes have a Table() method/,
            'cannot call next_as_hash when iterating with FakeUser class'
        );
        like(
            exception { $iterator->remaining_as_hashes() },
            qr/\QCannot make a hash unless all classes have a Table() method/,
            'cannot call remaining_as_hashes when iterating with FakeUser class'
        );
        like(
            exception { $iterator->all_as_hashes() },
            qr/\QCannot make a hash unless all classes have a Table() method/,
            'cannot call all_as_hashes when iterating with FakeUser class'
        );
    }

    if ( $class->can('raw_row') ) {
        my $sql
            = Fey::SQL->new_select->select(
            $schema->table('User')->columns(qw( user_id username email )) )
            ->from( $schema->table('User') )
            ->order_by( $schema->table('User')->column('user_id') );

        my $iterator = _make_iterator(
            $class,
            classes => 'User',
            dbh     => $dbh,
            select  => $sql,
        );

        $iterator->next();

        is_deeply(
            $iterator->raw_row(),
            [ 1, 'autarch', 'autarch@example.com' ],
            'raw_row returns expected data'
        );

        $iterator->next();

        is_deeply(
            $iterator->raw_row(),
            [ 42, 'bubba', 'bubba@example.com' ],
            'raw_row returns expected data'
        );

        $iterator->next();

        is(
            $iterator->raw_row(), undef,
            'raw_row returns undef when iterator is exhausted'
        );
    }
}

sub _make_iterator {
    my $class = shift;
    my %p     = @_;

    if ( $class eq 'Fey::Object::Iterator::FromArray' ) {
        my $from_select = Fey::Object::Iterator::FromSelect->new(%p);

        return $class->new(
            classes => $p{classes},
            objects => [ $from_select->all() ],
        );
    }
    else {
        return $class->new(%p);
    }
}

## no critic (Modules::ProhibitMultiplePackages)
{
    package FakeUser;

    use Moose;

    has [qw( user_id username email )] => ( is => 'ro' );

    no Moose;
}

1;