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

use FindBin;
use lib "$FindBin::Bin/lib";

use List::MoreUtils qw(all);
use Scalar::Util qw(looks_like_number);
use Storable qw(dclone);
use Test::AnyEvent::WebService::Tracks;
use Test::Exception;
use UNIVERSAL ();

my @orig_todos = ('Walk the dog', 'Finish TPS report', 'Take out the trash',
    'Install Tracks');
my @todos      = @orig_todos;

plan tests => 83 * @todos + 124;

my $tracks = get_tracks;

my $ctx;

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->contexts(sub {
        my ( $contexts ) = @_;

        unless(@$contexts) {
            $cond->send;
            return;
        }

        my $run;

        $run = sub {
            my $ctx = shift @$contexts;

            $ctx->destroy(sub {
                if(@$contexts) {
                    $run->();
                } else {
                    $cond->send;
                }
            });
        };
        $run->();
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->create_context(name => 'Test Context', sub {
        ( $ctx ) = @_;

        ok($ctx);
        $cond->send;
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;
    
    my $run;
    $run = sub {
        my $desc = shift @todos;

        $tracks->create_todo($desc, $ctx, sub {
            my ( $todo ) = @_;

            ok($todo);
            isa_ok($todo, 'AnyEvent::WebService::Tracks::Todo');
            is($todo->description, $desc);
            ok(! defined($todo->due));
            ok(! defined($todo->notes));
            ok(! defined($todo->show_from));
            ok($todo->is_active);
            ok(! $todo->is_project_hidden);
            ok(! $todo->is_complete);
            ok(! $todo->is_deferred);
            ok(looks_like_number $todo->id);
            ok(! $todo->completed_at);
            ok(! $todo->recurring_todo_id);
            isa_ok($todo->created_at, 'DateTime');
            isa_ok($todo->updated_at, 'DateTime');

            dies_ok {
                $todo->completed_at(DateTime->now);
            };
            dies_ok {
                $todo->created_at(DateTime->now);
            };
            dies_ok {
                $todo->updated_at(DateTime->now);
            };
            dies_ok {
                $todo->id(0);
            };
            dies_ok {
                $todo->recurring_todo_id(0);
            };

            lives_ok {
                $todo->description('Test description');
            };
            lives_ok {
                $todo->due(DateTime->now);
            };
            lives_ok {
                $todo->notes('Test notes');
            };
            lives_ok {
                $todo->show_from(DateTime->now);
            };

            $todo->context(sub {
                my ( $c ) = @_;

                is($c->id, $ctx->id);

                $todo->project(sub {
                    my ( $p, $e ) = @_;

                    ok(! defined($p));
                    ok(! defined($e));

                    if(@todos) {
                        $run->();
                    } else {
                        $cond->send;
                    }
                });
            });
        });
    };

    $run->();
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->todos(sub {
        my ( $todos ) = @_;

        is(scalar(@$todos), scalar(@orig_todos));
        ok(all { UNIVERSAL::isa($_, 'AnyEvent::WebService::Tracks::Todo') } @$todos);

        my $run;

        $run = sub {
            my $todo = shift @$todos;

            $todo->destroy(sub {
                my ( $ok ) = @_;

                ok($ok);

                if(@$todos) {
                    $run->();
                } else {
                    $cond->send;
                }
            });
        };

        $run->();
    });
};

@todos = @orig_todos;
run_tests_in_loop {
    my ( $cond ) = @_;
    
    my $run;
    $run = sub {
        my $desc = shift @todos;

        $tracks->create_todo(description => $desc, context => $ctx, sub {
            my ( $todo ) = @_;

            ok($todo);
            isa_ok($todo, 'AnyEvent::WebService::Tracks::Todo');
            is($todo->description, $desc);
            ok(! defined($todo->due));
            ok(! defined($todo->notes));
            ok(! defined($todo->show_from));
            ok($todo->is_active);
            ok(! $todo->is_project_hidden);
            ok(! $todo->is_complete);
            ok(! $todo->is_deferred);
            ok(looks_like_number $todo->id);
            ok(! $todo->completed_at);
            ok(! $todo->recurring_todo_id);
            isa_ok($todo->created_at, 'DateTime');
            isa_ok($todo->updated_at, 'DateTime');

            dies_ok {
                $todo->completed_at(DateTime->now);
            };
            dies_ok {
                $todo->created_at(DateTime->now);
            };
            dies_ok {
                $todo->updated_at(DateTime->now);
            };
            dies_ok {
                $todo->id(0);
            };
            dies_ok {
                $todo->recurring_todo_id(0);
            };

            lives_ok {
                $todo->description('Test description');
            };
            lives_ok {
                $todo->due(DateTime->now);
            };
            lives_ok {
                $todo->notes('Test notes');
            };
            lives_ok {
                $todo->show_from(DateTime->now);
            };

            $todo->context(sub {
                my ( $c ) = @_;

                is($c->id, $ctx->id);

                $todo->project(sub {
                    my ( $p, $e ) = @_;

                    ok(! defined($p));
                    ok(! defined($e));

                    if(@todos) {
                        $run->();
                    } else {
                        $cond->send;
                    }
                });
            });
        });
    };

    $run->();
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->todos(sub {
        my ( $todos ) = @_;

        my $run;

        $run = sub {
            my $todo = shift @$todos;

            $todo->destroy(sub {
                my ( $ok ) = @_;

                ok($ok);

                if(@$todos) {
                    $run->();
                } else {
                    $cond->send;
                }
            });
        };

        $run->();
    });
};

@todos = @orig_todos;
run_tests_in_loop {
    my ( $cond ) = @_;

    my $i = 1;
    
    my $run;
    $run = sub {
        my $desc = shift @todos;

        my $notes = "notes$i";

        $tracks->create_todo(description => $desc, notes => $notes, context => $ctx, sub {
            my ( $todo ) = @_;

            ok($todo);
            isa_ok($todo, 'AnyEvent::WebService::Tracks::Todo');
            is($todo->description, $desc);
            is($todo->notes, $notes);
            ok(! defined($todo->due));
            ok(! defined($todo->show_from));
            ok($todo->is_active);
            ok(! $todo->is_project_hidden);
            ok(! $todo->is_complete);
            ok(! $todo->is_deferred);
            ok(looks_like_number $todo->id);
            ok(! $todo->completed_at);
            ok(! $todo->recurring_todo_id);
            isa_ok($todo->created_at, 'DateTime');
            isa_ok($todo->updated_at, 'DateTime');

            dies_ok {
                $todo->completed_at(DateTime->now);
            };
            dies_ok {
                $todo->created_at(DateTime->now);
            };
            dies_ok {
                $todo->updated_at(DateTime->now);
            };
            dies_ok {
                $todo->id(0);
            };
            dies_ok {
                $todo->recurring_todo_id(0);
            };

            lives_ok {
                $todo->description('Test description');
            };
            lives_ok {
                $todo->due(DateTime->now);
            };
            lives_ok {
                $todo->notes('Test notes');
            };
            lives_ok {
                $todo->show_from(DateTime->now);
            };

            $todo->context(sub {
                my ( $c ) = @_;

                is($c->id, $ctx->id);

                $todo->project(sub {
                    my ( $p, $e ) = @_;

                    ok(! defined($p));
                    ok(! defined($e));

                    if(@todos) {
                        $run->();
                    } else {
                        $cond->send;
                    }
                });
            });
        });
    };

    $run->();
};

run_tests_in_loop {
    my ( $cond ) = @_;

    dies_ok {
        $tracks->create_todo(description => 'Foobar', sub {
            fail("I should never get called!");
        });
    };

    $cond->send;
};

run_tests_in_loop {
    my ( $cond ) = @_;

    dies_ok {
        $tracks->create_todo(description => 'Foobar', notes => 'Foobar', sub {
            fail("I should never be called!");
        });
    };
    $cond->send;
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->create_project('Test Project', sub {
        my ( $project ) = @_;

        ok($project);

        dies_ok {
            $tracks->create_todo(
                description => 'Foobar',
                context     => $ctx,
                project     => $ctx, sub {
                fail("I should never get called!");
            });

        };
        $cond->send;
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->projects(sub {
        my ( $projects ) = @_;

        my ( $project ) = @$projects;

        $project->default_context($ctx);

        $project->update(sub {
            my ( $p ) = @_;

            ok($p);
            $tracks->create_todo(
                description => 'Foobar',
                project     => $project, sub {
                
                my ( $todo, $err ) = @_;

                ok($todo) || diag($err);

                $todo->project(sub {
                    my ( $p ) = @_;

                    is($p->id, $project->id);

                    $tracks->create_todo('Foobar 2', $project, sub {
                        my ( $todo2 ) = @_;

                        ok($todo2);

                        $todo2->project(sub {
                            my ( $p2 ) = @_;

                            is($p2->id, $project->id);

                            dies_ok {
                                $tracks->create_todo(
                                    description => 'Foobar 3',
                                    project     => $ctx, sub {

                                    fail("I should never get called!");
                                });
                            };

                            $cond->send;
                        });
                    });
                });
            });
        });
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->projects(sub {
        my ( $projects ) = @_;

        my ( $project ) = @$projects;

        $project->default_context(undef);

        $project->update(sub {
            my ( $p ) = @_;

            ok($p);

            $tracks->create_todo(
                description => 'This should fail',
                project     => $project, sub {
                
                my ( $t ) = @_;

                ok(! $t);

                $cond->send;
            });
        });
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->todos(sub {
        my ( $todos ) = @_;

        my ( $one ) = @$todos;

        $one->destroy(sub {
            dies_ok {
                $one->is_complete;
            };
            dies_ok {
                $one->is_active;
            };
            dies_ok {
                $one->is_project_hidden;
            };
            dies_ok {
                $one->is_deferred;
            };
            dies_ok {
                $one->complete;
            };
            dies_ok {
                $one->activate;
            };
            dies_ok {
                $one->defer;
            };
            dies_ok {
                $one->description;
            };
            dies_ok {
                $one->notes;
            };
            dies_ok {
                $one->due;
            };
            dies_ok {
                $one->show_from;
            };
            dies_ok {
                $one->id;
            };
            dies_ok {
                $one->completed_at;
            };
            dies_ok {
                $one->recurring_todo_id;
            };
            dies_ok {
                $one->created_at;
            };
            dies_ok {
                $one->updated_at;
            };
            dies_ok {
                $one->context(sub {
                    fail("I shouldn't get called!");
                });
            };
            dies_ok {
                $one->project(sub {
                    fail("I shouldn't get called!");
                });
            };
            dies_ok {
                $one->update(sub {
                    fail("I shouldn't get called!");
                });
            };
            dies_ok {
                $one->destroy(sub {
                    fail("I shouldn't get called!");
                });
            };
            $cond->send;
        });
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->create_todo('Foobar 2', $ctx, sub {
        my ( $todo ) = @_;

        # we do this to make sure the update time is in the future
        my $timer;
        $timer = AnyEvent->timer(
            after => 1,
            cb => sub {
                undef $timer;
                my $old_updated = $todo->updated_at;

                $todo->description('Foobar 23');
                $todo->notes('Some notes!');

                is($todo->description, 'Foobar 23');
                is($todo->notes, 'Some notes!');

                $todo->update(sub {
                    my ( $todo2 ) = @_;

                    is($todo2, $todo);
                    is($todo2->description, 'Foobar 23');
                    is($todo2->notes, 'Some notes!');
                    isnt($todo2->updated_at, $old_updated);
                    ok($todo2->updated_at > $old_updated);

                    $cond->send;
                });
            },
        );
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->create_project('One', sub {
        my ( $one ) = @_;

        $tracks->create_project('Two', sub {
            my ( $two ) = @_;

            $tracks->create_todo(context => $ctx, project => $one, description => 'Test todo', sub {
                my ( $todo ) = @_;

                ok($todo);

                $todo->project(sub {
                    my ( $p ) = @_;

                    is($p->id, $one->id);

                    $todo->project($two);

                    $todo->update(sub {
                        my ( $todo ) = @_;

                        $todo->project(sub {
                            my ( $p ) = @_;

                            is($p->id, $two->id);

                            $todo->project(undef);

                            $todo->update(sub {
                                my ( $todo ) = @_;

                                $todo->project(sub {
                                    my ( $p ) = @_;

                                    ok(! $p);

                                    $todo->project($one);

                                    $todo->update(sub {
                                        my ( $todo ) = @_;

                                        $todo->project(sub {
                                            my ( $p ) = @_;

                                            is($p->id, $one->id);
                                            $cond->send;
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->create_context('Two', sub {
        my ( $two ) = @_;
        my $one = $ctx;

        ok($one);
        ok($two);

        $tracks->create_todo('Another test todo', $one, sub {
            my ( $todo ) = @_;

            $todo->context(sub {
                my ( $c ) = @_;

                is($c->id, $one->id);

                $todo->context($two);

                $todo->update(sub {
                    my ( $todo ) = @_;

                    $todo->context(sub {
                        my ( $c ) = @_;

                        is($c->id, $two->id);

                        dies_ok {
                            $todo->context(undef);
                        };
                        $cond->send;
                    });
                });
            });
        });
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    my @state_changes = (
        'deferred',  # active -> deferred
        'completed', # deferred -> completed
        'active',    # completed -> active
        'completed', # active -> completed
        'deferred',  # completed -> deferred
        'active',    # deferred -> active
    );

    $tracks->todos(sub {
        my ( $todos ) = @_;

        my ( $one ) = @$todos;

        my $run;

        $run = sub {
            my $change = shift @state_changes;

            if($change eq 'deferred') {
                $one->defer(1);
            } elsif($change eq 'active') {
                $one->activate;
            } else {
                $one->complete;
            }

            $one->update(sub {
                my ( $t ) = @_;

                ok($t);

                if($change eq 'deferred') {
                    ok(! $t->is_active);
                    ok($t->is_deferred);
                    ok(! $t->is_complete);
                    ok($t->show_from);
                } elsif($change eq 'active') {
                    ok($t->is_active);
                    ok(! $t->is_deferred);
                    ok(! $t->is_complete);
                    ok(! $t->show_from);
                } else {
                    ok(! $t->is_active);
                    ok(! $t->is_deferred);
                    ok($t->is_complete);
                    ok(! $t->show_from);
                }

                if(@state_changes) {
                    $run->();
                } else {
                    $cond->send;
                }
            });
        };

        $run->();
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    my $now      = DateTime->now;
    my $other    = $now->clone->add(days => 7, hours => 3, minutes => 1);
    my $duration = DateTime::Duration->new(days => 7, hours => 3, minutes => 1);
    my @defer    = (
        1         => $now->clone->add(days => 1),
        $other    => $other,
        $duration => $other,
    );

    $tracks->create_todo(
        description => 'Deferred Task',
        context     => $ctx,
        show_from   => $now->clone->add(days => 1),
    sub {
        my ( $todo ) = @_;

        ok($todo);
        ok($todo->is_deferred);
        ok($todo->show_from);
        my $expected = $now->clone->add(days => 1);
        if($expected->hour || $expected->minute || $expected->second) {
            $expected->set_hour(0);
            $expected->set_minute(0);
            $expected->set_second(0);
        };
        is($todo->show_from, $expected);

        my $run;

        $run = sub {
            my $arg      = shift @defer;
            my $expected = shift @defer;

            $todo->defer($arg);

            $todo->update(sub {
                my ( $t ) = @_;

                ok($t);
                ok($todo->is_deferred);
                ok($todo->show_from);
                if($expected->hour || $expected->minute || $expected->second) {
                    $expected->set_hour(0);
                    $expected->set_minute(0);
                    $expected->set_second(0);
                };
                is($todo->show_from, $expected);

                if(@defer) {
                    $run->();
                } else {
                    $cond->send;
                }
            });
        };

        $run->();
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    my $time = DateTime->now;
    $time->set_hour(17);
    $time->set_minute(0);
    $time->set_second(0);

    $tracks->create_todo(
        description => 'Another Deferred Task',
        context     => $ctx,
        show_from   => $time->clone->add(days => 1),
    sub {
        my ( $todo ) = @_;

        ok($todo);

        my $expected = $time->clone->add(days => 1);
        if($expected->hour || $expected->minute || $expected->second) {
            $expected->set_hour(0);
            $expected->set_minute(0);
            $expected->set_second(0);
        };
        is($todo->show_from, $expected);
        $cond->send;
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    my $time = DateTime->now;
    $time->set_hour(20);
    $time->set_minute(0);
    $time->set_second(0);

    $tracks->create_todo(
        description => 'Yet Another Deferred Task',
        context     => $ctx,
        show_from   => $time->clone->add(days => 1),
    sub {
        my ( $todo ) = @_;

        ok($todo);

        my $expected = $time->clone->add(days => 1);
        if($expected->hour || $expected->minute || $expected->second) {
            $expected->set_hour(0);
            $expected->set_minute(0);
            $expected->set_second(0);
        };
        is($todo->show_from, $expected);
        $cond->send;
    });
};

run_tests_in_loop {
    my ( $cond ) = @_;

    $tracks->todos(sub {
        my ( $todos ) = @_;

        my ( $one ) = @$todos;
        my $copy = dclone($one);

        $one->update(sub {
            my ( $t ) = @_;

            is_deeply($t, $copy);
            $cond->send;
        });
    });
};