#!/usr/bin/env perl

use strict;
use warnings;

use File::Basename 'dirname';
use File::Spec;

use lib join '/', File::Spec->splitdir( dirname(__FILE__) ), 'lib';
use lib join '/', File::Spec->splitdir( dirname(__FILE__) ), '..', 'lib';

#-------------------------------------------------------------------

# Define custom service
package MyService;

use Mojo::Base 'MojoX::JSON::RPC::Service';

sub multiply {
    my ( $self, @params ) = @_;
    my $m = 1;
    $m *= $_ for @params;

    return $m;
}

sub echo {
        my ( $self, @params ) = @_;

        return $params[0];
}

sub echo_key {
        my ( $self, $param ) = @_;

        return exists $param->{key} ? $param->{key} : '?';
}

sub register {
        my ($self) = @_;
        return 'register can be used';
}

sub _rpcs {
        my ($self) = @_;
        return '_rpcs can be used';
}

sub substract {
        my ( $self, $res, @params ) = @_;
        $res = 0 if !defined $res;
        $res -= $_ for @params;
        return $res;
}

sub update {
        my ( $self, @params ) = @_;

        # notification only

        return;
}

sub foobar {
        my ($self) = @_;

        # notification only

        return;
}

__PACKAGE__->register_rpc_method_names(
        'multiply', 'echo',      'echo_key', 'register',
        '_rpcs',    'substract', 'update',   'foobar'
);

#-------------------------------------------------------------------

# Mojolicious app for testing
package MojoxJsonRpc;

use Mojo::Base 'Mojolicious';

use MojoX::JSON::RPC::Service;

# This method will run once at server start
sub startup {
        my $self = shift;

        $self->secret('Testing!');

        # Load our test plugin
        my $svc = MojoX::JSON::RPC::Service->new->register(
            'sum',
            sub {
                my @params = @_;
                my $sum    = 0;
                $sum += $_ for @params;
                return $sum;
            }
            )->register(
            'remote_address',
            sub {
                my $tx = shift;
                return $tx->remote_address;
            },
            { with_mojo_tx => 1 }
            )->register(
            'substract',
            sub {
                my $params = shift;

                return $params->{minuend} - $params->{subtrahend};
            },
            )->register(
            'subtract',
            sub {
                my ( $res, @params ) = @_;
                $res = 0 if !defined $res;
                $res -= $_ for @params;
                return $res;
            },
            )->register(
            'notify_hello',
            sub {
                return;
            }
            )->register(
            'notify_sum',
            sub {
                return;
            }
            )->register(
            'get_data',
            sub {
                return [ 'hello', 5 ];
            }
            );

        # register multiple services
        $self->plugin(
            'json_rpc_dispatcher',
            services => {
                '/jsonrpc'  => $svc,
                '/jsonrpc2' => MyService->new
            }
        );
}

#-------------------------------------------------------------------

# Back to tests
package main;

use TestUts;

use Test::More tests => 33;
use Test::Mojo;

use_ok 'MojoX::JSON::RPC::Service';
use_ok 'MojoX::JSON::RPC::Client';

my $t = Test::Mojo->new( 'MojoxJsonRpc' );
my $client = MojoX::JSON::RPC::Client->new( ua => $t->ua );
my $res;

TestUts::test_call(
        $client,
        '/jsonrpc',
        {   id     => 1,
            method => 'sum',
            params => [ 17, 25 ]
        },
        {   result => 42,
            id     => 1
        },
        'sum 1'
);

# Test second service url
TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 2,
            method => 'multiply',
            params => [ 2, 3 ]
        },
        {   result => 6,
            id     => 2
        },
        'multiply 1'
);

# Can call register which is also defined in MojoX::JSON::RPC::Service
TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 2,
            method => 'register',
        },
        {   result => 'register can be used',
            id     => 2
        },
        'register 1'
);

TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 2,
            method => 'echo',
            params => ['HEEEEEEEEEEEEEEEEELLLLLLLLLLLLLLLOOOOOOOOOOO!']
        },
        {   result => 'HEEEEEEEEEEEEEEEEELLLLLLLLLLLLLLLOOOOOOOOOOO!',
            id     => 2
        },
        'echo 1'
);

# params must be array!
TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 2,
            method => 'echo',
            params => 'HEEEEEEEEEEEEEEEEELLLLLLLLLLLLLLLOOOOOOOOOOO!'
        },
        {   error => {
                code    => -32602,
                message => 'Invalid params.',
                data =>
                    'NOT array or hash: HEEEEEEEEEEEEEEEEELLLLLLLLLLLLLLLOOOOOOOOOOO!'
            }
        },
        'echo 2'
);

TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 2,
            method => 'echo',
            params => [ 'a' x 32768 ]
        },
        {   result => 'a' x 32768,
            id     => 2
        },
        'echo 3'
);

# Can call _rpcs which is an attribute of MojoX::JSON::RPC::Service
TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 2,
            method => '_rpcs',
        },
        { result => '_rpcs can be used', },
        '_rpcs 1'
);

# Test rpc call that get tx added on server side.
TestUts::test_call(
        $client,
        '/jsonrpc',
        {   id     => 2,
            method => 'remote_address',
        },
        { result => '127.0.0.1', },
        'remote_address 1'
);

# Test hash param
TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 2,
            method => 'echo_key',
            params =>
                { key => 'HEEEEEEEEEEEEEEEEELLLLLLLLLLLLLLLOOOOOOOOOOO!' }
        },
        { result => 'HEEEEEEEEEEEEEEEEELLLLLLLLLLLLLLLOOOOOOOOOOO!', },
        'echo_key 1'
);

# GET test
TestUts::test_call(
        $client, '/jsonrpc?method=sum;params=[2,3,5];id=1',
        undef, { results => 10 },
        'GET sum 1'
);

TestUts::test_call(
        $client,
        '/jsonrpc?',
        undef,
        {   error => {
                code    => -32600,
                message => 'Invalid Request.'
            }
        },
        'GET no method'
);

# Test client proxy

my $proxy = $client->prepare(
        '/jsonrpc'  => 'sum',
        '/jsonrpc2' => [ 'multiply', 'echo' ]
);

is( $proxy->sum( 1, 2 ), 3, 'proxy sum 1' );
is( $proxy->multiply( 1, 2, 3 ), 6, 'proxy multiply 1' );
is( $proxy->echo('Testing 1 2 3'), 'Testing 1 2 3', 'proxy echo 1' );

### Test non blocking

my $loop_count = 1;

my $nb_test1;
my $nb_test2;

$client->call(
        '/jsonrpc',
        {   id     => 2,
            method => 'sum',
            params => [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
        },
        sub {
            my $res = pop;

            $nb_test1 = $res->result;
            if ( $loop_count-- == 0 ) { Mojo::IOLoop->stop; }
        }
);

$client->call(
        '/jsonrpc2',
        {   id     => 2,
            method => 'echo',
            params => ['..................................']
        },
        sub {
            my $res = pop;

            $nb_test2 = $res->result;
            if ( $loop_count-- == 0 ) { Mojo::IOLoop->stop; }
        }
);
Mojo::IOLoop->start;

is( $nb_test1, 45, 'Non blocking 1' );
is( $nb_test2, '..................................', 'Non blocking 2' );

######## Some extra tests from spec

#rpc call with positional parameters:
#--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}
#<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 1,
            method => 'substract',
            params => [ 42, 23 ]
        },
        {   result => 19,
            id     => 1
        },
        'rpc call with positional parameters 1'
);

#--> {"jsonrpc": "2.0", "method": "subtract", "params": [23, 42], "id": 2}
#<-- {"jsonrpc": "2.0", "result": -19, "id": 2}
TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   id     => 2,
            method => 'substract',
            params => [ 23, 42 ]
        },
        {   id     => 2,
            result => -19,
        },
        'rpc call with positional parameters 2'
);

#rpc call with named parameters:
#--> {"jsonrpc": "2.0", "method": "subtract", "params": {"subtrahend": 23, "minuend": 42}, "id": 3}
#<-- {"jsonrpc": "2.0", "result": 19, "id": 3}

TestUts::test_call(
        $client,
        '/jsonrpc',
        {   id     => 3,
            method => 'substract',
            params => {
                minuend    => 42,
                subtrahend => 23,
            }
        },
        {   id     => 3,
            result => 19,
        },
        'rpc call with named parameters 1'
);

#--> {"jsonrpc": "2.0", "method": "subtract", "params": {"minuend": 42, "subtrahend": 23}, "id": 4}
#<-- {"jsonrpc": "2.0", "result": 19, "id": 4}

TestUts::test_call(
        $client,
        '/jsonrpc',
        {   id     => 4,
            method => 'substract',
            params => {
                minuend    => 23,
                subtrahend => 42,
            }
        },
        {   id     => 4,
            result => -19,
        },
        'rpc call with named parameters 2'
);

#a Notification:
#--> {"jsonrpc": "2.0", "method": "update", "params": [1,2,3,4,5]}

TestUts::test_call(
        $client,
        '/jsonrpc2',
        {   method => 'update',
            params => [ 1, 2, 3, 4, 5 ]
        },
        {},
        'a Notification 1'
);

#--> {"jsonrpc": "2.0", "method": "foobar"}
TestUts::test_call( $client, '/jsonrpc2', { method => 'foobar', },
        {}, 'a Notification 2' );

#rpc call of non-existent method:
#--> {"jsonrpc": "2.0", "method": "foobar", "id": "1"}
#<-- {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Procedure not found."}, "id": "1"}
TestUts::test_call(
        $client,
        '/jsonrpc',
        {   id     => 1,
            method => 'foobar'
        },
        {   error => {
                code    => -32601,
                message => 'Method not found.'
            },
            id => 1
        },
        'rpc call of non-existent method'
);

#rpc call with invalid JSON:
#--> {"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]
#<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error."}, "id": null}
TestUts::test_call(
        $client,
        '/jsonrpc',
        q|{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]|,
        {   error => {
                code    => -32700,
                message => 'Parse error.'
            },
            id => undef
        },
        'rpc call with invalid JSON'
);

#rpc call with invalid Request object:
#--> {"jsonrpc": "2.0", "method": 1, "params": "bar"}
#<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
TestUts::test_call(
        $client,
        '/jsonrpc',
        {   id     => 1,
            method => 1,
            params => 'bar'
        },
        {   error => {
                code    => -32600,
                message => 'Invalid Request.'
            },
            id => undef
        },
        'rpc call with invalid Request object'
);

#rpc call Batch, invalid JSON:
#--> [ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},{"jsonrpc": "2.0", "method" ]
#<-- {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error."}, "id": null}
TestUts::test_call(
        $client,
        '/jsonrpc',
        q|[ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},{"jsonrpc": "2.0", "method" ]|,
        {   error => {
                code    => -32700,
                message => 'Parse error.'
            },
            id => undef
        },
        'rpc call Batch, invalid JSON'
);

#rpc call with an empty Array:
#--> []
#<-- {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}

TestUts::test_call(
        $client,
        '/jsonrpc',
        q|[]|,
        {   error => {
                code    => -32600,
                message => 'Invalid Request.'
            },
            id => undef
        },
        'rpc call with an empty Array'
);

#rpc call with an invalid Batch (but not empty):
#--> [1]
#<-- [
#        {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
#    ]
TestUts::test_call(
        $client,
        '/jsonrpc',
        q|[1]|,
        {   error => {
                code    => -32600,
                message => 'Invalid Request.'
            },
            id => undef
        },
        'rpc call with an invalid Batch (but not empty)'
);

#rpc call with invalid Batch:
#--> [1,2,3]
#<-- [
#        {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null},
#        {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null},
#        {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null}
#    ]
TestUts::test_call(
        $client,
        '/jsonrpc',
        q|[1,2,3]|,
        [   {   error => {
                    code    => -32600,
                    message => 'Invalid Request.'
                },
                id => undef
            },
            {   error => {
                    code    => -32600,
                    message => 'Invalid Request.'
                },
                id => undef
            },
            {   error => {
                    code    => -32600,
                    message => 'Invalid Request.'
                },
                id => undef
            },
        ],
        'rpc call with an invalid Batch'
);

#rpc call Batch:
#--> [
#        {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
#        {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
#        {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
#        {"foo": "boo"},
#        {"jsonrpc": "2.0", "method": "foo.get", "params": {"name": "myself"}, "id": "5"},
#        {"jsonrpc": "2.0", "method": "get_data", "id": "9"}
#    ]
#<-- [
#        {"jsonrpc": "2.0", "result": 7, "id": "1"},
#        {"jsonrpc": "2.0", "result": 19, "id": "2"},
#        {"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request."}, "id": null},
#        {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": "5"},
#        {"jsonrpc": "2.0", "result": ["hello", 5], "id": "9"}
#    ]

TestUts::test_call(
        $client,
        '/jsonrpc',
        [   { method => 'sum',          params => [ 1, 2, 4 ], id => '1' },
            { method => 'notify_hello', params => [7] },
            { method => 'subtract', params => [ 42, 23 ], id => '2' },
            { foo    => 'boo' },
            {   method => 'foo.get',
                params => { name => 'myself' },
                id     => '5'
            },
            { method => 'get_data', id => '9' }
        ],
        [   {   result => 7,
                id     => 1
            },
            {   result => 19,
                id     => 2
            },
            {   error => {
                    code    => -32600,
                    message => 'Invalid Request.'
                },
                id => undef
            },
            {   error => {
                    code    => -32601,
                    message => 'Method not found.'
                },
                id => 5
            },
            {   result => [ 'hello', 5 ],
                id     => 9
            },
        ],
        'rpc call Batch'
);

#rpc call Batch (all notifications):
#--> [
#        {"jsonrpc": "2.0", "method": "notify_sum", "params": [1,2,4]},
#        {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]},
#    ]
#<-- //Nothing is returned for all notification batches
TestUts::test_call
(
    $client,
    '/jsonrpc',
    [   { method => 'notify_sum',   params => [ 1, 2, 4 ] },
        { method => 'notify_hello', params => [7] },
    ],
    {},
    'rpc call Batch (all notifications)'
);

1;
