'''
Define the details of inter-actor "out-of-band" (OoB) cancel
semantics, that is how cancellation works when a cancel request comes
from the different concurrency (primitive's) "layer" then where the
eventual `trio.Task` actually raises a signal.

'''
from functools import partial
# from contextlib import asynccontextmanager as acm
# import itertools

import pytest
import trio
import tractor
from tractor import (  # typing
    ActorNursery,
    Portal,
    Context,
    # ContextCancelled,
    # RemoteActorError,
)
# from tractor._testing import (
#     tractor_test,
#     expect_ctxc,
# )

# XXX TODO cases:
# - [ ] peer cancelled itself - so other peers should
#   get errors reflecting that the peer was itself the .canceller?

# def test_self_cancel():
#     '''
#     2 cases:
#     - calls `Actor.cancel()` locally in some task
#     - calls LocalPortal.cancel_actor()` ?
#
# things to ensure!
# -[ ] the ctxc raised in a child should ideally show the tb of the
#     underlying `Cancelled` checkpoint, i.e.
#     `raise scope_error from ctxc`?
#
# -[ ] a self-cancelled context, if not allowed to block on
#     `ctx.result()` at some point will hang since the `ctx._scope`
#     is never `.cancel_called`; cases for this include,
#     - an `open_ctx()` which never starteds before being OoB actor
#       cancelled.
#       |_ parent task will be blocked in `.open_context()` for the
#         `Started` msg, and when the OoB ctxc arrives `ctx._scope`
#         will never have been signalled..

#     '''
#     ...

# TODO, sanity test against the case in `/examples/trio/lockacquire_not_unmasked.py`
# but with the `Lock.acquire()` from a `@context` to ensure the
# implicit ignore-case-non-unmasking.
#
# @tractor.context
# async def acquire_actor_global_lock(
#     ctx: tractor.Context,
#     ignore_special_cases: bool,
# ):

#     async with maybe_unmask_excs(
#         ignore_special_cases=ignore_special_cases,
#     ):
#         await ctx.started('locked')

#     # block til cancelled
#     await trio.sleep_forever()


@tractor.context
async def sleep_forever(
    ctx: tractor.Context,
    # ignore_special_cases: bool,
    do_started: bool,
):

    # async with maybe_unmask_excs(
    #     ignore_special_cases=ignore_special_cases,
    # ):
    #     await ctx.started('locked')
    if do_started:
        await ctx.started()

    # block til cancelled
    print('sleepin on child-side..')
    await trio.sleep_forever()


@pytest.mark.parametrize(
    'cancel_ctx',
    [True, False],
)
def test_cancel_ctx_with_parent_side_entered_in_bg_task(
    debug_mode: bool,
    loglevel: str,
    cancel_ctx: bool,
):
    '''
    The most "basic" out-of-band-task self-cancellation case where
    `Portal.open_context()` is entered in a bg task and the
    parent-task (of the containing nursery) calls `Context.cancel()`
    without the child knowing; the `Context._scope` should be
    `.cancel_called` when the IPC ctx's child-side relays
    a `ContextCancelled` with a `.canceller` set to the parent
    actor('s task).

    '''
    async def main():
        with trio.fail_after(
            2 if not debug_mode else 999,
        ):
            an: ActorNursery
            async with (
                tractor.open_nursery(
                    debug_mode=debug_mode,
                    loglevel='devx',
                    enable_stack_on_sig=True,
                ) as an,
                trio.open_nursery() as tn,
            ):
                ptl: Portal = await an.start_actor(
                    'sub',
                    enable_modules=[__name__],
                )

                async def _open_ctx_async(
                    do_started: bool = True,
                    task_status=trio.TASK_STATUS_IGNORED,
                ):
                    # do we expect to never enter the
                    # `.open_context()` below.
                    if not do_started:
                        task_status.started()

                    async with ptl.open_context(
                        sleep_forever,
                        do_started=do_started,
                    ) as (ctx, first):
                        task_status.started(ctx)
                        await trio.sleep_forever()

                # XXX, this is the key OoB part!
                #
                # - start the `.open_context()` in a bg task which
                #   blocks inside the embedded scope-body,
                #
                # -  when we call `Context.cancel()` it **is
                #   not** from the same task which eventually runs
                #   `.__aexit__()`,
                #
                # - since the bg "opener" task will be in
                #   a `trio.sleep_forever()`, it must be interrupted
                #   by the `ContextCancelled` delivered from the
                #   child-side; `Context._scope: CancelScope` MUST
                #   be `.cancel_called`!
                #
                print('ASYNC opening IPC context in subtask..')
                maybe_ctx: Context|None = await tn.start(partial(
                    _open_ctx_async,
                ))

                if (
                    maybe_ctx
                    and
                    cancel_ctx
                ):
                    print('cancelling first IPC ctx!')
                    await maybe_ctx.cancel()

                # XXX, note that despite `maybe_context.cancel()`
                # being called above, it's the parent (bg) task
                # which was originally never interrupted in
                # the `ctx._scope` body due to missing case logic in
                # `ctx._maybe_cancel_and_set_remote_error()`.
                #
                # It didn't matter that the subactor process was
                # already terminated and reaped, nothing was
                # cancelling the ctx-parent task's scope!
                #
                print('cancelling subactor!')
                await ptl.cancel_actor()

                if maybe_ctx:
                    try:
                        await maybe_ctx.wait_for_result()
                    except tractor.ContextCancelled as ctxc:
                        assert not cancel_ctx
                        assert (
                            ctxc.canceller
                            ==
                            tractor.current_actor().aid.uid
                        )
                        # don't re-raise since it'll trigger
                        # an EG from the above tn.

    if cancel_ctx:
        # graceful self-cancel
        trio.run(main)

    else:
        # ctx parent task should see OoB ctxc due to
        # `ptl.cancel_actor()`.
        with pytest.raises(tractor.ContextCancelled) as excinfo:
            trio.run(main)

        assert 'root' in excinfo.value.canceller[0]


# def test_parent_actor_cancels_subactor_with_gt1_ctxs_open_to_it(
#     debug_mode: bool,
#     loglevel: str,
# ):
#     '''
#     Demos OoB cancellation from the perspective of a ctx opened with
#     a child subactor where the parent cancels the child at the "actor
#     layer" using `Portal.cancel_actor()` and thus the
#     `ContextCancelled.canceller` received by the ctx's parent-side
#     task will appear to be a "self cancellation" even though that
#     specific task itself was not cancelled and thus
#     `Context.cancel_called ==False`.
#     '''
                # TODO, do we have an existing implied ctx
                # cancel test like this?
                # with trio.move_on_after(0.5):# as cs:
                #     await _open_ctx_async(
                #         do_started=False,
                #     )


                # in-line ctx scope should definitely raise
                # a ctxc with `.canceller = 'root'`
                # async with ptl.open_context(
                #     sleep_forever,
                #     do_started=True,
                # ) as pair:

