import inspect
from typing import Dict, Any
from eth2spec.utils.ssz.ssz_typing import View
from eth2spec.utils.ssz.ssz_impl import serialize


def vector_test(description: str = None):
    """
    vector_test decorator: Allow a caller to pass "generator_mode=True" to make the test yield data,
     but behave like a normal test (ignoring the yield, but fully processing) a test when not in "generator_mode"
    This should always be the most outer decorator around functions that yield data.
    This is to deal with silent iteration through yielding function when in a pytest
     context (i.e. not in generator mode).
    :param description: Optional description for the test to add to the metadata.
    :return: Decorator.
    """
    def runner(fn):
        # this wraps the function, to yield type-annotated entries of data.
        # Valid types are:
        #   - "meta": all key-values with this type can be collected by the generator, to put somewhere together.
        #   - "ssz": raw SSZ bytes
        #   - "data": a python structure to be encoded by the user.
        def entry(*args, **kw):

            def generator_mode():
                if description is not None:
                    # description can be explicit
                    yield 'description', 'meta', description

                # transform the yielded data, and add type annotations
                for data in fn(*args, **kw):
                    # if not 2 items, then it is assumed to be already formatted with a type:
                    # e.g. ("bls_setting", "meta", 1)
                    if len(data) != 2:
                        yield data
                        continue
                    # Try to infer the type, but keep it as-is if it's not a SSZ type or bytes.
                    (key, value) = data
                    if value is None:
                        continue
                    if isinstance(value, View):
                        yield key, 'ssz', serialize(value)
                    elif isinstance(value, bytes):
                        yield key, 'ssz', value
                    elif isinstance(value, list) and all([isinstance(el, (View, bytes)) for el in value]):
                        for i, el in enumerate(value):
                            if isinstance(el, View):
                                yield f'{key}_{i}', 'ssz', serialize(el)
                            elif isinstance(el, bytes):
                                yield f'{key}_{i}', 'ssz', el
                        yield f'{key}_count', 'meta', len(value)
                    else:
                        # Not a ssz value.
                        # The data will now just be yielded as any python data,
                        #  something that should be encodable by the generator runner.
                        yield key, 'data', value

            # check generator mode, may be None/else.
            # "pop" removes it, so it is not passed to the inner function.
            if kw.pop('generator_mode', False) is True:
                # return the yielding function as a generator object.
                # Don't yield in this function itself, that would make pytest skip over it.
                return generator_mode()
            else:
                # Just complete the function, ignore all yielded data,
                # we are not using it (or processing it, i.e. nearly zero efficiency loss)
                # Pytest does not support yielded data in the outer function, so we need to wrap it like this.
                for _ in fn(*args, **kw):
                    continue
                return None

        return entry

    return runner


def with_meta_tags(tags: Dict[str, Any]):
    """
    Decorator factory, yields meta tags (key, value) pairs to the output of the function.
    Useful to build test-vector annotations with.
    :param tags: dict of tags
    :return: Decorator.
    """
    def runner(fn):
        def entry(*args, **kw):
            yielded_any = False
            for part in fn(*args, **kw):
                yield part
                yielded_any = True
            # Do not add tags if the function is not returning a dict at all (i.e. not in generator mode).
            # As a pytest, we do not want to be yielding anything (unsupported by pytest)
            if yielded_any:
                for k, v in tags.items():
                    yield k, 'meta', v
        return entry
    return runner


def build_transition_test(fn, pre_fork_name, post_fork_name, fork_epoch=None):
    """
    Handles the inner plumbing to generate `transition_test`s.
    See that decorator in `context.py` for more information.
    """
    def _adapter(*args, **kwargs):
        post_spec = kwargs["phases"][post_fork_name]

        pre_fork_counter = 0

        def pre_tag(obj):
            nonlocal pre_fork_counter
            pre_fork_counter += 1
            return obj

        def post_tag(obj):
            return obj

        yield "post_fork", "meta", post_fork_name

        has_fork_epoch = False
        if fork_epoch:
            kwargs["fork_epoch"] = fork_epoch
            has_fork_epoch = True
            yield "fork_epoch", "meta", fork_epoch

        # massage args to handle an optional custom state using
        # `with_custom_state` decorator
        expected_args = inspect.getfullargspec(fn)
        if "phases" not in expected_args.kwonlyargs:
            kwargs.pop("phases", None)

        for part in fn(*args,
                       post_spec=post_spec,
                       pre_tag=pre_tag,
                       post_tag=post_tag,
                       **kwargs):
            if part[0] == "fork_epoch":
                has_fork_epoch = True
            yield part
        assert has_fork_epoch

        if pre_fork_counter > 0:
            yield "fork_block", "meta", pre_fork_counter - 1
    return _adapter
