# t/comment.t
#
# Test LJ::Comment.
#
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.

use strict;
use warnings;

use Test::More tests => 405;

BEGIN { $LJ::_T_CONFIG = 1; require "$ENV{LJHOME}/cgi-bin/ljlib.pl"; }
use LJ::Utils qw(rand_chars);
use LJ::Protocol;
use LJ::Comment;
use LJ::Talk;
use LJ::Test qw(memcache_stress temp_user);
use POSIX ();

my $u = temp_user();

sub run_tests {

    # constructor tests
    {
        my $c;

        $c = eval { LJ::Comment->new( {}, jtalkid => 1 ) };
        like( $@, qr/invalid journalid/, "invalid journalid parameter" );

        $c = eval { LJ::Comment->new( 0, jtalkid => 1 ) };
        like( $@, qr/invalid journalid/, "invalid user from userid" );

        $c = eval { LJ::Comment->new( $u, jtalkid => 1, 'foo' ) };
        like( $@, qr/wrong number/, "wrong number of arguments" );

        $c = eval { LJ::Comment->new( $u, jtalkid => undef ) };
        like( $@, qr/need to supply jtalkid/, "need to supply jtalkid" );

        $c = eval { LJ::Comment->new( $u, jtalkid => 1, foo => 1, bar => 2 ) };
        like( $@, qr/wrong number/, "wrong number of arguments (unknown parameters)" );
    }

    # post a comment
    {
        my $e1 = $u->t_post_fake_entry;
        ok( $e1, "Posted entry" );

        my $c1 = $e1->t_enter_comment;
        ok( $c1, "Posted comment" );

        # check that the comment happened in the last 60 seconds
        my $c1time = $c1->unixtime;
        ok( $c1time,                             "Got comment time" );
        ok( POSIX::abs( $c1time - time() ) < 60, "Comment happened in last minute" );
    }

    # test prop setting/modifying/deleting
    {
        my $e2 = $u->t_post_fake_entry;
        my $c2 = $e2->t_enter_comment;

        # set a prop once, then re-set its value again
        my $jtalkid = $c2->jtalkid;

        # FIXME the whole idea of using undef as one of the loop values seem to make the code
        # more complex, check if this can be changed
        foreach my $propval ( 0, 1, undef, 1 ) {

            # re-instantiate if we've blown $c2 away
            $c2 ||= LJ::Comment->new( $u, jtalkid => $jtalkid );

            my $inserted = 0;
            $LJ::_T_COMMENT_SET_PROPS_INSERT = sub { $inserted++ };
            my $deleted = 0;
            $LJ::_T_COMMENT_SET_PROPS_DELETE = sub { $deleted++ };

            $c2->set_prop( 'opt_preformatted', $propval );

            if ( defined $propval ) {
                ok( $inserted == 1 && $deleted == 0, "$propval: Inserted talkprop row prop-erly" );
            }
            else {
                ok( $deleted == 1 && $inserted == 0, "undef: Deleted talkprop row prop-erly" );
            }

            is( $c2->prop('opt_preformatted'),
                $propval,
                ( defined $propval ? $propval : 'undef' ) . ": Set prop and read back via ->prop" );
            is( $c2->props->{opt_preformatted}, $propval,
                ( defined $propval ? $propval : 'undef' )
                    . ": Set prop and read back via ->props" );

            # clear the singleton and load again
            LJ::Comment->reset_singletons;
            my $loaded = 0;
            $LJ::_T_GET_TALK_PROPS2_MEMCACHE = sub { $loaded++ };

            my $c2_new  = LJ::Comment->new( $u, jtalkid => $jtalkid );
            my $propval = $c2_new->prop('opt_preformatted');
            ok(
                $loaded == 1,
                ( defined $propval ? $propval : 'undef' )
                    . ", Re-instantiated comment and re-loaded prop"
            );
            ok(
                $c2_new != $c2,
                ( defined $propval ? $propval : 'undef' )
                    . ", Re-instantiated comment and re-loaded prop"
            );
        }

        # test raw prop setting/modifying
        {
            # re-instantiate if we've blown $c2 away
            $c2 ||= LJ::Comment->new( $u, jtalkid => $jtalkid );

            my $inserted = 0;
            $LJ::_T_COMMENT_SET_PROPS_INSERT = sub { $inserted++ };
            my $deleted = 0;
            $LJ::_T_COMMENT_SET_PROPS_DELETE = sub { $deleted++ };

            $c2->set_prop_raw( 'edit_time', "UNIX_TIMESTAMP()" );

            ok( $inserted == 1 && $deleted == 0, "Inserted raw talkprop row prop-erly" );

            ok( $c2->prop('edit_time')  =~ /^\d+$/, "Set raw prop and read back via ->prop" );
            ok( $c2->props->{edit_time} =~ /^\d+$/, "Set raw prop and read back via ->props" );

            # clear the singleton and load again
            LJ::Comment->reset_singletons;
            my $loaded = 0;
            $LJ::_T_GET_TALK_PROPS2_MEMCACHE = sub { $loaded++ };

            my $c2_new  = LJ::Comment->new( $u, jtalkid => $jtalkid );
            my $propval = $c2_new->prop('edit_time');
            ok(
                $loaded == 1 && $c2_new != $c2 && $propval == $propval,
                "Re-instantiated comment and re-loaded raw prop"
            );
        }

        # test prop multi-setting/modifying/deleting
        # test prop setting/modifying/deleting
        {
            my $inserted = 0;
            $LJ::_T_COMMENT_SET_PROPS_INSERT = sub { $inserted++ };
            my $deleted = 0;
            $LJ::_T_COMMENT_SET_PROPS_DELETE = sub { $deleted++ };
            my $queried = 0;
            $LJ::_T_GET_TALK_PROPS2_MEMCACHE = sub { $queried++ };

            {    # both inserts
                my $e3 = $u->t_post_fake_entry;
                my $c3 = $e3->t_enter_comment;

                $c3->set_props( 'opt_preformatted' => 1, 'picture_keyword' => 2 );
                ok(
                    $c3->prop('opt_preformatted') == 1
                        && $c3->prop('picture_keyword') == 2
                        && $inserted == 1
                        && $deleted == 0
                        && $queried == 1,
                    "Set 2 props and read back"
                );
            }

            ( $inserted, $deleted, $queried ) = ( 0, 0, 0 );

            {    # mixed
                my $e4 = $u->t_post_fake_entry;
                my $c4 = $e4->t_enter_comment;

                $c4->set_props( 'opt_preformatted' => undef, 'picture_keyword' => 2 );
                ok(
                    !defined( $c4->prop('opt_preformatted') )
                        && $c4->prop('picture_keyword') == 2
                        && $inserted == 1
                        && $deleted == 1
                        && $queried == 1,
                    "Set 1 prop, deleted 1, and read back"
                );
            }

            ( $inserted, $deleted, $queried ) = ( 0, 0, 0 );

            {    # deletes
                my $e5 = $u->t_post_fake_entry;
                my $c5 = $e5->t_enter_comment;

                $c5->set_props( 'opt_preformatted' => undef, 'picture_keyword' => undef );
                ok(
                    !defined( $c5->prop('opt_preformatted') )
                        && !defined( $c5->prop('picture_keyword') )
                        && $inserted == 0
                        && $deleted == 1
                        && $queried == 1,
                    "Set 1 prop, deleted 1, and read back"
                );
            }

            ( $inserted, $deleted, $queried ) = ( 0, 0, 0 );

            {    # raw
                my $e6 = $u->t_post_fake_entry;
                my $c6 = $e6->t_enter_comment;

                $c6->set_props_raw( 'edit_time' => "UNIX_TIMESTAMP()", 'opt_preformatted' => 1 );
                ok(
                    $c6->prop('opt_preformatted') == 1
                        && $c6->prop('edit_time') =~ /^\d+$/
                        && $inserted == 1
                        && $deleted == 0
                        && $queried == 1,
                    "Set 2 raw props and read back"
                );
            }
        }
    }

    # post a tree of comments
    {

        # step counter so we can test multiple legacy API interactions
        foreach my $step ( 0 .. 2 ) {

            my $entry = $u->t_post_fake_entry;

            # entry
            # - child 0
            # - child 1
            #   + child 1.1
            # - child 2
            #   + child 2.1
            #   + child 2.2
            # - child 3
            #   + child 3.1
            #   + child 3.2
            #   + child 3.3
            # - child 4
            #   + child 4.1
            #   + child 4.2
            #   + child 4.3
            #   + child 4.4
            # - child 5
            #   + child 5.1
            #   + child 5.2
            #   + child 5.3
            #   + child 5.4
            #   + child 5.5

            my @tree = ();    # [ child => [ sub_children ]

            # create 5 comments on this entry
            foreach my $top_reply_ct ( 0 .. 5 ) {
                my $c = $entry->t_enter_comment;

                my $curr = [ $c => [] ];
                push @tree, $curr;

                # now make 5 replies to each comment, except for the first
                foreach my $reply_ct ( 1 .. $top_reply_ct ) {
                    last if $top_reply_ct == 0;

                    my $child = $c->t_reply;
                    $child->set_prop( 'opt_preformatted', 1 );
                    push @{ $curr->[1] }, $child;
                }
            }

            # are the first-level children created properly?
            ok( @tree == 6, "$step: Created 6 child comments" );

            # how about subchildren?
            my %want = map { $_ => 1 } 0 .. 5;
            delete $want{ scalar @{ $_->[1] } } foreach @tree;
            ok( !%want, "$step: Created 0..5 sub-child comments" );

            # test accesses to ->children methods in cases where legacy APIs are also called
            my %access_ct = ();
            $LJ::_T_GET_TALK_DATA_MEMCACHE   = sub { $access_ct{data}++ };
            $LJ::_T_GET_TALK_TEXT2_MEMCACHE  = sub { $access_ct{text}++ };
            $LJ::_T_GET_TALK_PROPS2_MEMCACHE = sub { $access_ct{props}++ };

            # 0: straight call to ->children
            # 1: get_talk_data call
            # 2: $entry->comment_list call
            # 3: load_comments call
            LJ::Talk::get_talk_data( $u, 'L', $entry->jitemid ) if $step == 1;
            $entry->comment_list if $step == 2;
            LJ::Talk::load_comments( $u, undef, 'L', $entry->jitemid ) if $step == 3;

            %want = map { $_ => 1 } 0 .. 5;
            foreach my $parent ( map { $_->[0] } @tree ) {

                my @children = $parent->children;
                delete $want{ scalar @children };

                # now access text and props
                # FIXME: should test case of legacy prop/text access, then object method
                $parent->props;
                $parent->body_raw;

                foreach my $child (@children) {
                    $child->props;
                    $child->subject_raw;
                }
            }
            ok( !%want,                "$step: Retrieved 0..5 sub-children LJ::Comment objects" );
            ok( $access_ct{data} == 1, "$step: Only one talk data access with legacy interaction" );
            ok( $access_ct{text} == 1, "$step: Only one text data access with legacy interaction" );
            ok( $access_ct{props} == 1,
                "$step: Only one prop data access with legacy interaction" );

            # Add to the tree:
            # - child 6
            #   + child 6.1
            #     + child 6.1.1
            #       + child 6.1.1.1
            my $comment = $entry->t_enter_comment;
            my $curr    = [ $comment => [] ];
            push @tree, $curr;
            foreach ( 1 .. 3 ) {
                $comment = $comment->t_reply;
                push @{ $curr->[1] }, $comment;
            }

            # look up root
            foreach my $parent ( map { $_->[0] } @tree ) {
                ok(
                    $parent->threadrootid eq $parent->jtalkid,
                    "Comment depth 1: this is the thread root"
                );

                my @children = $parent->children;
                foreach my $child (@children) {
                    ok( $child->parenttalkid == $child->threadrootid,
                        "Comment depth 2: thread root and parent are equivalent." );

                    my $descendant = $child;
                    my $depth      = 2;
                    foreach ( $descendant->children ) {
                        ok(
                            $child->parenttalkid == $descendant->threadrootid,
"Comment depth $depth: thread root is no longer directly linked to this comment."
                        );

                        $depth++;
                        $descendant = $_;
                    }
                }
            }
        }
    }

    # test editing of comment text
    {
        my $e = $u->t_post_fake_entry;
        my $c = $e->t_enter_comment;

        my $jtalkid     = $c->jtalkid;
        my $old_subject = $c->subject_raw;
        my $old_body    = $c->body_raw;

        {
            my $new_subject = LJ::rand_chars(25);
            my $new_body    = LJ::rand_chars(500);
            $c->set_subject($new_subject);
            ok( $c->subject_raw eq $new_subject && $c->body_raw eq $old_body, "Set subject okay" );

            $c->set_body($new_body);
            ok( $c->subject_raw eq $new_subject && $c->body_raw eq $new_body, "Set body okay" );

            # clear out and check memcache
            LJ::Comment->reset_singletons;

            $c = LJ::Comment->new( $u, jtalkid => $jtalkid );
            ok( $c->subject_raw eq $new_subject && $c->body_raw eq $new_body,
                "Read subject and body back from memcache" );
        }

        {
            my $new_subject = LJ::rand_chars(25);
            my $new_body    = LJ::rand_chars(500);

            $c->set_subject_and_body( $new_subject, $new_body );
            ok( $c->subject_raw eq $new_subject && $c->body_raw eq $new_body,
                "Set subject and body at once" );

            # clear out and check memcache
            LJ::Comment->reset_singletons;

            $c = LJ::Comment->new( $u, jtalkid => $jtalkid );
            ok( $c->subject_raw eq $new_subject && $c->body_raw eq $new_body,
                "Read subject and body back from memcache" );

        }

        # test setting of subejct / body with unknown8bit set
        {
            $c->set_prop( 'unknown8bit', 1 );

            eval { $c->set_subject($old_subject) };
            ok( $@ =~ 'unknown8bit', "Can't set unknown8bit without subject / body" );

            eval { $c->set_subject_and_body( $old_subject, $old_body ) };
            ok( !$@ && $c->subject_raw eq $old_subject && $c->body_raw eq $old_body,
                "Able to set unknown8bit with subject and body" );
            ok( $c->prop('unknown8bit') == 0, "unknown8bit prop unset" );
        }
    }
}

memcache_stress {
    run_tests();
};

1;

