"""Module for a test cases that delegates execution to a callable object."""

import inspect
import logging

from asteex.core.case.executable.abstract import ExecutableCase, ExecutionError
from asteex.core.decorator.argument import ContextMapper, CopyError


class BuildError(Exception):
    """Exception for errors raised on attempt to build a test case."""


class DelegateCase(ExecutableCase):
    """Class for a asteex test case based on a callable."""

    def __init__(self, delegate, case_id, dependencies=()):
        super().__init__()

        if not callable(delegate):
            raise ValueError("Can't create a delegate case from a non-callable object")
        self.__delegate = delegate

        self.__id = str(case_id)

        logger = logging.getLogger(__name__)
        logger.debug("Try to refine dependencies for a new delegate case.")
        try:
            dependencies = tuple(str(dependency) for dependency in dependencies)
        except TypeError as error:
            logger.exception("Can't iterate over dependencies of a new delegate case.")
            raise ValueError("Can't iterate over dependencies of a new delegate case") from error
        self.__dependencies = dependencies
        logger.debug("Refined dependencies for a new delegate case.")

    def get_id(self):
        """Get the unique identifier of the test case.

        :returns: string id.
        """
        return self.__id

    def get_dependencies(self):
        """Get names of the required variables.

        :returns: tuple with string names of the required variables.
        """
        return self.__dependencies

    async def execute(self, context):
        """Execute the test case.

        :param context: dictionary with string keys.
        :raises: :class:`ExecutionError <asteex.core.case.execution.abstract.ExecutionError>`.
        """
        logger = logging.getLogger(__name__)

        logger.debug("Try to execute the delegate of the case %s.", self)
        try:
            execution = self.__delegate(context=context)

            if inspect.isawaitable(execution):
                await execution
        except Exception as error:
            logger.exception("An error is raised during the execution.")
            raise ExecutionError("An error is raised during the execution") from error

        logger.info("Executed the delegate of the case %s.", self)


class DelegateCaseBuilder(ContextMapper):
    """Class to create delegate cases from callables."""

    def __init__(self):
        super().__init__()
        self.__case_id = None
        self.__additional_dependencies = frozenset()
        self.__enhancers = ()

    def set_case_id(self, case_id):
        """Set identifier for new test cases.

        :param case_id: string identifier or None.
            None value implies that the builder has to deduce case identifiers from callables.
        :returns: self.
        """
        if case_id is not None:
            case_id = str(case_id)
        self.__case_id = case_id

        logger = logging.getLogger(__name__)
        logger.info("Set case identifier in %s.", self)

        return self

    def get_case_id(self):
        """Get identifier for new test cases.

        :returns: string identifier or None.
        """
        return self.__case_id

    def add_dependencies(self, dependencies, overwrite=False):
        """Add dependencies.

        :param dependencies: iterable with string names of the dependencies.
        :param overwrite: boolean whether the new dependencies should overwrite the existing ones.
        :returns: self.
        :raises: :class:`ValueError` if dependencies is not a valid iterable.
        """
        logger = logging.getLogger(__name__)
        logger.debug("Try to set additional case dependencies in %s.", self)

        try:
            refined_case_dependencies = frozenset(str(dependency) for dependency in dependencies)
        except TypeError as error:
            logger.exception("Can't iterate over case dependencies.")
            raise ValueError("Can't iterate over case dependencies") from error

        if overwrite:
            self.__additional_dependencies = refined_case_dependencies
        else:
            self.__additional_dependencies = self.__additional_dependencies.union(
                refined_case_dependencies,
            )

        logger.info("Set additional case dependencies in %s.", self)

        return self

    def get_additional_dependencies(self):
        """Get additional dependencies.

        :returns: set with string names of the dependencies.
        """
        return self.__additional_dependencies

    def add_enhancers(self, enhancers, overwrite=False):
        """Add enhancers for new test cases.

        :param enhancers: iterable with callables that accepts two parameters. Each callable will
            receive a :class:`DelegateCase` for the first parameter and a callable for
            the second parameter. The function must return :class:`DelegateCase`.
        :param overwrite: boolean whether the new enhancers should overwrite the existing ones.
        :returns: self.
        :raises: :class:`ValueError` if enhancers is not a valid iterable.
        """
        logger = logging.getLogger(__name__)
        logger.debug("Try to set case enhancers in %s.", self)

        try:
            refined_case_enhancers = tuple(enhancers)
        except TypeError as error:
            logger.exception("Can't iterate over case enhancers.")
            raise ValueError("Can't iterate over case enhancers") from error

        if overwrite:
            self.__enhancers = refined_case_enhancers
        else:
            self.__enhancers = self.__enhancers + refined_case_enhancers

        logger.info("Set case enhancers in %s.", self)

        return self

    def get_enhancers(self):
        """Get enhancers for new test cases.

        :returns: tuple with callables that accepts two parameters. Each callable accepts
            a :class:`DelegateCase` for the first parameter and a callable for the second parameter.
            Each callable returns :class:`DelegateCase`.
        """
        return self.__enhancers

    def build_from_callable(self, callable_):
        """Create a delegate case from a callable.

        :param callable_: a callable to use as a base for the delegate case.
        :returns: a new :class`DelegateCase`.
        :raises: :class:`BuildError` if an error occurs on attempt to create or enhance a case.
        """
        logger = logging.getLogger(__name__)
        logger.debug("Try to create a delegate case from '%s'.", callable_)

        try:
            case = self._create_case(callable_)
        except Exception as error:
            logger.exception("Can't create a delegate case.")
            raise BuildError("Can't create a delegate case") from error

        for enhancer in self.get_enhancers():
            try:
                case = enhancer(case, callable_)
            except Exception as error:
                logger.exception("Can't enhance a delegate case.")
                raise BuildError("Can't enhance a delegate case") from error

        logger.info("Created a delegate case from '%s'.", callable_)
        return case

    def _copy_data(self, other):
        """Copy data from this instance to another.

        :param other: instance of the same type.
        :raises: :class:`CopyError <asteex.core.decorator.argument.CopyError>`
            if data can't be copied.
        """
        super()._copy_data(other)

        logger = logging.getLogger(__name__)

        try:
            other.set_case_id(self.get_case_id())
        except Exception as error:
            logger.exception("Can't copy case identifier to another instance.")
            raise CopyError("Can't copy case identifier to another instance") from error

        try:
            other.add_dependencies(self.get_additional_dependencies(), overwrite=True)
        except Exception as error:
            logger.exception("Can't copy case dependencies to another instance.")
            raise CopyError("Can't copy case dependencies to another instance") from error

        try:
            other.add_enhancers(self.get_enhancers(), overwrite=True)
        except Exception as error:
            logger.exception("Can't copy case enhancers to another instance.")
            raise CopyError("Can't copy case enhancers to another instance") from error

    def _create_case_id(self, callable_):
        """Create an identifier for a delegate case from a callable.

        :param callable_: callable for a delegate for the case.
        :returns: string id.
        """
        case_id = self.get_case_id()
        if case_id is None:
            refined_case_id = str(callable_)
        else:
            refined_case_id = case_id

        return refined_case_id

    def _create_case_dependencies(self, callable_):
        """Create dependencies for a delegate case from a callable.

        :param callable_: callable for a delegate for the case.
        :returns: set with string names.
        """
        case_dependencies = set(self.get_additional_dependencies())

        positional_arguments, keyword_arguments = self.build_call_arguments(callable_)

        arguments_with_dependencies = set().union(positional_arguments).union(keyword_arguments)
        arguments_with_dependencies.difference_update(self.get_arguments_mapped_to_context())

        dependency_map = self.get_arguments_mapped_to_context_names()

        case_dependencies.update(
            dependency_map.get(argument, argument) for argument in arguments_with_dependencies
        )

        return case_dependencies

    def _create_case_delegate(self, callable_):
        """Create a delegate for a delegate case from a callable.

        :param callable_: callable for a delegate for the case.
        :returns: callable, suitable for :class:`DelegateCase`.
        """
        arguments_mapped_to_context_names = self.get_arguments_mapped_to_context_names().copy()
        arguments_mapped_to_context = set(self.get_arguments_mapped_to_context())

        positional_arguments, keyword_arguments = self.build_call_arguments(callable_)

        def _delegate(context):
            def _get_delegate_value(argument):
                if argument in arguments_mapped_to_context:
                    value = context
                else:
                    mapped_argument = arguments_mapped_to_context_names.get(argument, argument)
                    value = context[mapped_argument]

                return value

            arg_values = [_get_delegate_value(argument) for argument in positional_arguments]
            kwarg_values = {
                argument: _get_delegate_value(argument)
                for argument in keyword_arguments
            }

            return callable_(*arg_values, **kwarg_values)

        return _delegate

    def _create_case(self, callable_):
        """Create a delegate case from a callable.

        :param callable_: callable for a delegate for the case.
        :returns: :class:`DelegateCase`.
        """
        return DelegateCase(
            delegate=self._create_case_delegate(callable_),
            case_id=self._create_case_id(callable_),
            dependencies=self._create_case_dependencies(callable_),
        )
