"""
RPC related
"""
import itertools

import pytest
import tractor
import trio


async def sleep_back_actor(
    actor_name,
    func_name,
    func_defined,
    exposed_mods,
):
    if actor_name:
        async with tractor.find_actor(actor_name) as portal:
            try:
                await portal.run(__name__, func_name)
            except tractor.RemoteActorError as err:
                if not func_defined:
                    expect = AttributeError
                if not exposed_mods:
                    expect = tractor.ModuleNotExposed

                assert err.type is expect
                raise
    else:
        await trio.sleep(float('inf'))


async def short_sleep():
    await trio.sleep(0)


@pytest.mark.parametrize(
    'to_call', [
        ([], 'short_sleep', tractor.RemoteActorError),
        ([__name__], 'short_sleep', tractor.RemoteActorError),
        ([__name__], 'fake_func', tractor.RemoteActorError),
        (['tmp_mod'], 'import doggy', ModuleNotFoundError),
        (['tmp_mod'], '4doggy', SyntaxError),
    ],
    ids=['no_mods', 'this_mod', 'this_mod_bad_func', 'fail_to_import',
         'fail_on_syntax'],
)
def test_rpc_errors(arb_addr, to_call, testdir):
    """Test errors when making various RPC requests to an actor
    that either doesn't have the requested module exposed or doesn't define
    the named function.
    """
    exposed_mods, funcname, inside_err = to_call
    subactor_exposed_mods = []
    func_defined = globals().get(funcname, False)
    subactor_requests_to = 'root'
    remote_err = tractor.RemoteActorError

    # remote module that fails at import time
    if exposed_mods == ['tmp_mod']:
        # create an importable module with a bad import
        testdir.syspathinsert()
        # module should raise a ModuleNotFoundError at import
        testdir.makefile('.py', tmp_mod=funcname)

        # no need to expose module to the subactor
        subactor_exposed_mods = exposed_mods
        exposed_mods = []
        func_defined = False
        # subactor should not try to invoke anything
        subactor_requests_to = None
        # the module will be attempted to be imported locally but will
        # fail in the initial local instance of the actor
        remote_err = inside_err

    async def main():

        # spawn a subactor which calls us back
        async with tractor.open_nursery(
            arbiter_addr=arb_addr,
            enable_modules=exposed_mods.copy(),
        ) as n:

            actor = tractor.current_actor()
            assert actor.is_arbiter
            await n.run_in_actor(
                sleep_back_actor,
                actor_name=subactor_requests_to,

                name='subactor',

                # function from the local exposed module space
                # the subactor will invoke when it RPCs back to this actor
                func_name=funcname,
                exposed_mods=exposed_mods,
                func_defined=True if func_defined else False,
                enable_modules=subactor_exposed_mods,
            )

    def run():
        trio.run(main)

    # handle both parameterized cases
    if exposed_mods and func_defined:
        run()
    else:
        # underlying errors aren't propagated upwards (yet)
        with pytest.raises(remote_err) as err:
            run()

        # get raw instance from pytest wrapper
        value = err.value

        # might get multiple `trio.Cancelled`s as well inside an inception
        if isinstance(value, trio.MultiError):
            value = next(itertools.dropwhile(
                lambda exc: not isinstance(exc, tractor.RemoteActorError),
                value.exceptions
            ))

        if getattr(value, 'type', None):
            assert value.type is inside_err