from pyln.client import Plugin
from pyln.client.plugin import Request, Millisatoshi, RpcException
import itertools
import pytest  # type: ignore


def test_simple_methods():
    """Test the dispatch of methods, with a variety of bindings.
    """
    call_list = []
    p = Plugin(autopatch=False)

    @p.method("test1")
    def test1(name):
        """Has a single positional argument."""
        assert name == 'World'
        call_list.append(test1)
    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test1',
        'params': {'name': 'World'}
    })
    p._dispatch_request(request)
    assert call_list == [test1]

    @p.method("test2")
    def test2(name, plugin):
        """Also asks for the plugin instance. """
        assert plugin == p
        call_list.append(test2)
    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test2',
        'params': {'name': 'World'}
    })
    p._dispatch_request(request)
    assert call_list == [test1, test2]

    @p.method("test3")
    def test3(name, request):
        """Also asks for the request instance. """
        assert request is not None
        call_list.append(test3)
    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test3',
        'params': {'name': 'World'}
    })
    p._dispatch_request(request)
    assert call_list == [test1, test2, test3]

    @p.method("test4")
    def test4(name):
        """Try the positional arguments."""
        assert name == 'World'
        call_list.append(test4)
    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test4',
        'params': ['World']
    })
    p._dispatch_request(request)
    assert call_list == [test1, test2, test3, test4]

    @p.method("test5")
    def test5(name, request, plugin):
        """Try the positional arguments, mixing in the request and plugin."""
        assert name == 'World'
        assert request is not None
        assert p == plugin
        call_list.append(test5)
    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test5',
        'params': ['World']
    })
    p._dispatch_request(request)
    assert call_list == [test1, test2, test3, test4, test5]

    answers = []

    @p.method("test6")
    def test6(name, answer=42):
        """This method has a default value for one of its params"""
        assert name == 'World'
        answers.append(answer)
        call_list.append(test6)

    # Both calls should work (with and without the default param
    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test6',
        'params': ['World']
    })
    p._dispatch_request(request)
    assert call_list == [test1, test2, test3, test4, test5, test6]
    assert answers == [42]

    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test6',
        'params': ['World', 31337]
    })
    p._dispatch_request(request)
    assert call_list == [test1, test2, test3, test4, test5, test6, test6]
    assert answers == [42, 31337]


def test_methods_errors():
    """A bunch of tests that should fail calling the methods."""
    call_list = []
    p = Plugin(autopatch=False)

    # Fails because we haven't added the method yet
    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test1',
        'params': {}
    })
    with pytest.raises(ValueError):
        p._dispatch_request(request)
    assert call_list == []

    @p.method("test1")
    def test1(name):
        call_list.append(test1)

    # Attempting to add it twice should fail
    with pytest.raises(ValueError):
        p.add_method("test1", test1)

    # Fails because it is missing the 'name' argument
    request = p._parse_request({'id': 1, 'jsonrpc': '2.0', 'method': 'test1', 'params': {}})
    with pytest.raises(TypeError):
        p._exec_func(test1, request)
    assert call_list == []

    # The same with positional arguments
    request = p._parse_request({'id': 1, 'jsonrpc': '2.0', 'method': 'test1', 'params': []})
    with pytest.raises(TypeError):
        p._exec_func(test1, request)
    assert call_list == []

    # Fails because we have a non-matching argument
    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test1',
        'params': {'name': 'World', 'extra': 1}
    })
    with pytest.raises(TypeError):
        p._exec_func(test1, request)
    assert call_list == []

    request = p._parse_request({
        'id': 1,
        'jsonrpc': '2.0',
        'method': 'test1',
        'params': ['World', 1]
    })

    with pytest.raises(TypeError):
        p._exec_func(test1, request)
    assert call_list == []


def test_method_exceptions():
    """A bunch of tests that should fail calling the methods."""
    p = Plugin(autopatch=False)

    def fake_write_result(resultdict):
        global result_dict
        result_dict = resultdict

    @p.method("test_raise")
    def test_raise():
        raise RpcException("testing RpcException", code=-1000)

    req = Request(p, 1, "test_raise", {})
    req._write_result = fake_write_result
    p._dispatch_request(req)
    assert result_dict['jsonrpc'] == '2.0'
    assert result_dict['id'] == 1
    assert result_dict['error']['code'] == -1000
    assert result_dict['error']['message'] == "testing RpcException"

    @p.method("test_raise2")
    def test_raise2():
        raise Exception("normal exception")

    req = Request(p, 1, "test_raise2", {})
    req._write_result = fake_write_result
    p._dispatch_request(req)
    assert result_dict['jsonrpc'] == '2.0'
    assert result_dict['id'] == 1
    assert result_dict['error']['code'] == -32600
    assert result_dict['error']['message'] == "Error while processing test_raise2: normal exception"


def test_positional_inject():
    p = Plugin()
    rdict = Request(
        plugin=p,
        req_id=1,
        method='func',
        params={'a': 1, 'b': 2, 'kwa': 3, 'kwb': 4}
    )
    rarr = Request(
        plugin=p,
        req_id=1,
        method='func',
        params=[1, 2, 3, 4],
    )

    def pre_args(plugin, a, b, kwa=3, kwb=4):
        assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4)

    def in_args(a, plugin, b, kwa=3, kwb=4):
        assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4)

    def post_args(a, b, plugin, kwa=3, kwb=4):
        assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4)

    def post_kwargs(a, b, kwa=3, kwb=4, plugin=None):
        assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4)

    def in_multi_args(a, request, plugin, b, kwa=3, kwb=4):
        assert request in [rarr, rdict]
        assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4)

    def in_multi_mix_args(a, plugin, b, request=None, kwa=3, kwb=4):
        assert request in [rarr, rdict]
        assert (plugin, a, b, kwa, kwb) == (p, 1, 2, 3, 4)

    def extra_def_arg(a, b, c, d, e=42):
        """ Also uses a different name for kwa and kwb
        """
        assert (a, b, c, d, e) == (1, 2, 3, 4, 42)

    def count(plugin, count, request):
        assert count == 42 and plugin == p

    funcs = [pre_args, in_args, post_args, post_kwargs, in_multi_args]

    for func, request in itertools.product(funcs, [rdict, rarr]):
        p._exec_func(func, request)

    p._exec_func(extra_def_arg, rarr)

    p._exec_func(count, Request(
        plugin=p,
        req_id=1,
        method='func',
        params=[42],
    ))

    # This should fail since it is missing one positional argument
    with pytest.raises(TypeError):
        p._exec_func(count, Request(
            plugin=p,
            req_id=1,
            method='func',
            params=[])
        )


def test_bind_pos():
    p = Plugin(autopatch=False)

    req = object()
    params = ['World']

    def test1(name):
        assert name == 'World'
    bound = p._bind_pos(test1, params, req)
    test1(*bound.args, **bound.kwargs)

    def test2(name, plugin):
        assert name == 'World'
        assert plugin == p
    bound = p._bind_pos(test2, params, req)
    test2(*bound.args, **bound.kwargs)

    def test3(plugin, name):
        assert name == 'World'
        assert plugin == p
    bound = p._bind_pos(test3, params, req)
    test3(*bound.args, **bound.kwargs)

    def test4(plugin, name, request):
        assert name == 'World'
        assert plugin == p
        assert request == req
    bound = p._bind_pos(test4, params, req)
    test4(*bound.args, **bound.kwargs)

    def test5(request, name, plugin):
        assert name == 'World'
        assert plugin == p
        assert request == req
    bound = p._bind_pos(test5, params, req)
    test5(*bound.args, **bound.kwargs)

    def test6(request, name, plugin, answer=42):
        assert name == 'World'
        assert plugin == p
        assert request == req
        assert answer == 42
    bound = p._bind_pos(test6, params, req)
    test6(*bound.args, **bound.kwargs)

    # Now mix in a catch-all parameter that needs to be assigned
    def test6(request, name, plugin, *args, **kwargs):
        assert name == 'World'
        assert plugin == p
        assert request == req
        assert args == (42,)
        assert kwargs == {}
    bound = p._bind_pos(test6, params + [42], req)
    test6(*bound.args, **bound.kwargs)


def test_bind_kwargs():
    p = Plugin(autopatch=False)

    req = object()
    params = {'name': 'World'}

    def test1(name):
        assert name == 'World'
    bound = p._bind_kwargs(test1, params, req)
    test1(*bound.args, **bound.kwargs)

    def test2(name, plugin):
        assert name == 'World'
        assert plugin == p
    bound = p._bind_kwargs(test2, params, req)
    test2(*bound.args, **bound.kwargs)

    def test3(plugin, name):
        assert name == 'World'
        assert plugin == p
    bound = p._bind_kwargs(test3, params, req)
    test3(*bound.args, **bound.kwargs)

    def test4(plugin, name, request):
        assert name == 'World'
        assert plugin == p
        assert request == req
    bound = p._bind_kwargs(test4, params, req)
    test4(*bound.args, **bound.kwargs)

    def test5(request, name, plugin):
        assert name == 'World'
        assert plugin == p
        assert request == req
    bound = p._bind_kwargs(test5, params, req)
    test5(*bound.args, **bound.kwargs)

    def test6(request, name, plugin, answer=42):
        assert name == 'World'
        assert plugin == p
        assert request == req
        assert answer == 42
    bound = p._bind_kwargs(test6, params, req)
    test6(*bound.args, **bound.kwargs)

    # Now mix in a catch-all parameter that needs to be assigned
    def test6(request, name, plugin, *args, **kwargs):
        assert name == 'World'
        assert plugin == p
        assert request == req
        assert args == ()
        assert kwargs == {'answer': 42}
    bound = p._bind_kwargs(test6, {'name': 'World', 'answer': 42}, req)
    test6(*bound.args, **bound.kwargs)


def test_argument_coercion():
    p = Plugin(autopatch=False)

    def test1(msat: Millisatoshi):
        assert isinstance(msat, Millisatoshi)

    ba = p._bind_kwargs(test1, {"msat": "100msat"}, None)
    test1(*ba.args)

    ba = p._bind_pos(test1, ["100msat"], None)
    test1(*ba.args, **ba.kwargs)


def test_duplicate_result():
    p = Plugin(autopatch=False)

    def test1(request):
        request.set_result(1)     # MARKER1
        request.set_result(1)

    req = Request(p, req_id=1, method="test1", params=[])
    ba = p._bind_kwargs(test1, {}, req)
    with pytest.raises(ValueError, match=r'current state is RequestState\.FINISHED(.*\n.*)*MARKER1'):
        test1(*ba.args)

    def test2(request):
        request.set_exception(1)  # MARKER2
        request.set_exception(1)

    req = Request(p, req_id=2, method="test2", params=[])
    ba = p._bind_kwargs(test2, {}, req)
    with pytest.raises(ValueError, match=r'current state is RequestState\.FAILED(.*\n*.*)*MARKER2'):
        test2(*ba.args)

    def test3(request):
        request.set_exception(1)  # MARKER3
        request.set_result(1)

    req = Request(p, req_id=3, method="test3", params=[])
    ba = p._bind_kwargs(test3, {}, req)
    with pytest.raises(ValueError, match=r'current state is RequestState\.FAILED(.*\n*.*)*MARKER3'):
        test3(*ba.args)

    def test4(request):
        request.set_result(1)     # MARKER4
        request.set_exception(1)

    req = Request(p, req_id=4, method="test4", params=[])
    ba = p._bind_kwargs(test4, {}, req)
    with pytest.raises(ValueError, match=r'current state is RequestState\.FINISHED(.*\n*.*)*MARKER4'):
        test4(*ba.args)
