import inspect
import os
from abc import abstractmethod
from typing import Any, Dict, Tuple, Optional, Type
from unittest import TestCase

from ._AbstractTestMeta import AbstractTestMeta
from .serialisation import BytesSerialiser, StringSerialiser, RegressionSerialiser
from ._functions import get_subject_args, get_skip_reason, get_serialisers, is_test

# The default location to store regression test results
DEFAULT_REGRESSION_ROOT = os.path.join(".", "resources", "regression")


class AbstractTest(TestCase, metaclass=AbstractTestMeta):
    def __init__(self, methodName='runTest'):
        # Get the method this instance is testing
        method = getattr(self, methodName)

        # If it's a marked test, it may have been given a
        # prefix, so substitute the base method's name
        if is_test(method):
            methodName = method.__name__

        super().__init__(methodName)

    @classmethod
    @abstractmethod
    def subject_type(cls):
        """
        Defines the subject callable for this test case.
        """
        pass

    @classmethod
    def common_resources(cls) -> Optional[Tuple[Any, ...]]:
        """
        Loads the common resources for all tests in this class.
        By default there are no common resources.
        """
        return None

    @classmethod
    def common_serialisers(cls) -> Optional[Dict[Type, Type[RegressionSerialiser]]]:
        """
        Defines additional common serialisers to use for regression
        tests in this class (in addition to the basic string and bytes
        serialisers, although these can be overridden).
        """
        return None

    @classmethod
    def common_arguments(cls) -> Optional[Tuple[Tuple[Any, ...], Dict[str, Any]]]:
        """
        Defines the default arguments to pass to the subject on instantiation.
        By default, no arguments are passed.
        """
        return None

    def setUp(self) -> None:
        # Get the method being tested
        test_method = self.get_test_method()

        # If the method is marked with skip, skip it
        skip_reason = get_skip_reason(test_method)
        if skip_reason is not None:
            self.skipTest(skip_reason)

    @classmethod
    def instantiate_subject(cls, *args, **kwargs) -> Any:
        """
        Instantiates a test subject with the given arguments.
        """
        return cls.subject_type()(*args, **kwargs)

    def subject(self) -> Any:
        # Get the arguments to use to create the test subject from the test method
        subject_args = get_subject_args(self.get_test_method())

        # If there are arguments, use them
        if subject_args is not None:
            return self.instantiate_subject(*subject_args[0], **subject_args[1])

        # If not, try using the default arguments
        subject_args = self.common_arguments()

        # If there are default arguments, use them
        if subject_args is not None:
            return self.instantiate_subject(*subject_args[0], **subject_args[1])

        # Otherwise use the default constructor
        return self.instantiate_subject()

    def handle_regression_results(self, results):
        """
        Handles comparing the results of a regression test to
        the results stored in the regression files.

        :param results:     The results of the regression test.
        """
        # Make sure the regression results are a named map
        if not isinstance(results, dict) or not all(isinstance(key, str) for key in results):
            self.fail("Regression test '" +
                      self.get_test_method_name() +
                      "' didn't return a named map of regression results")

        # Handle each result individually
        for name, result in results.items():
            self.handle_regression_result(name, result)

    def handle_regression_result(self, name: str, result: Any):
        """
        Handles the comparison of an individual regression result
        to the stored reference.

        :param name:    The name of the regression to use.
        :param result:  The result of the regression test.
        """
        # Treat each individual regression result as a sub-test
        with self.subTest(regression=name):
            # Get the serialisers for this test method
            serialisers = self.get_serialisers()

            # Get the preference order of serialiser types
            mro = inspect.getmro(type(result))

            # Get a serialiser for this result type
            serialiser = None
            for result_type in mro:
                if result_type in serialisers:
                    serialiser = serialisers[result_type]
                    break

            # Make sure we have a serialiser
            if serialiser is None:
                self.fail("No regression serialiser found for result of type: " + type(result).__name__)

            # Complete the file path for this result
            filename: str = os.path.join(self.get_regression_path(), self.get_test_method_name(), name)

            # If the regression file doesn't exist yet, create it
            if not serialiser.exists(filename):
                serialiser.save(result, filename)

            # Otherwise load and check the saved result
            else:
                reference = serialiser.load(filename)

                # Use the serialiser's notion of equality
                failure_message: str = serialiser.compare(result, reference)
                if failure_message is not None:
                    self.fail(serialiser.__name__ + ": " + failure_message)

    @classmethod
    def get_regression_root_path(cls) -> str:
        """
        Gets the path to the root directory to store regression results in.
        Can be overridden for a custom directory per test-class.

        :return:    The path to the root directory.
        """
        return DEFAULT_REGRESSION_ROOT

    @classmethod
    def get_regression_path(cls) -> str:
        """
        Gets the path to the regression results for this test-class.

        :return:    The path.
        """
        return os.path.join(cls.get_regression_root_path(), cls.get_relative_regression_path())

    @classmethod
    def get_relative_regression_path(cls) -> str:
        """
        Gets the path to the regression results for this test-class,
        relative to the root path.

        :return:    The path.
        """
        # Get the fully-qualified name of the subject (in dotted form)
        fully_qualified_name: str = cls.subject_type().__module__ + '.' + cls.subject_type().__qualname__

        # Replace the dots with platform-dependent slashes
        return fully_qualified_name.replace(".", os.sep)

    def get_serialisers(self) -> Dict[Type, Type[RegressionSerialiser]]:
        """
        Gets the serialisers to use for this test method.

        :return:    The map from type to serialiser.
        """
        # Define the basic serialisers
        serialisers = {
            str: StringSerialiser,
            bytes: BytesSerialiser
        }

        # Add the common serialisers for this class
        common_serialisers = self.common_serialisers()
        if common_serialisers is not None:
            serialisers.update(common_serialisers)

        # Override with any test-specific serialisers
        serialisers.update(get_serialisers(self.get_test_method()))

        return serialisers

    def get_test_method(self):
        """
        Gets the test method being tested by this test instance.

        :return:    The test method.
        """
        return getattr(self, self.get_test_method_name())

    def get_test_method_name(self) -> str:
        """
        Gets the name of the test method being tested by this test instance.

        :return:    The name of the test method.
        """
        return self._testMethodName

import inspect
import os
from abc import abstractmethod
from typing import Any, Dict, Tuple, Optional, Type
from unittest import TestCase

from ._AbstractTestMeta import AbstractTestMeta
from .serialisation import BytesSerialiser, StringSerialiser, RegressionSerialiser
from ._functions import get_subject_args, get_skip_reason, get_serialisers, is_test

# The default location to store regression test results
DEFAULT_REGRESSION_ROOT = os.path.join(".", "resources", "regression")


class AbstractTest(TestCase, metaclass=AbstractTestMeta):
    def __init__(self, methodName='runTest'):
        # Get the method this instance is testing
        method = getattr(self, methodName)

        # If it's a marked test, it may have been given a
        # prefix, so substitute the base method's name
        if is_test(method):
            methodName = method.__name__

        super().__init__(methodName)

    @classmethod
    @abstractmethod
    def subject_type(cls):
        """
        Defines the subject callable for this test case.
        """
        pass

    @classmethod
    def common_resources(cls) -> Optional[Tuple[Any, ...]]:
        """
        Loads the common resources for all tests in this class.
        By default there are no common resources.
        """
        return None

    @classmethod
    def common_serialisers(cls) -> Optional[Dict[Type, Type[RegressionSerialiser]]]:
        """
        Defines additional common serialisers to use for regression
        tests in this class (in addition to the basic string and bytes
        serialisers, although these can be overridden).
        """
        return None

    @classmethod
    def common_arguments(cls) -> Optional[Tuple[Tuple[Any, ...], Dict[str, Any]]]:
        """
        Defines the default arguments to pass to the subject on instantiation.
        By default, no arguments are passed.
        """
        return None

    def setUp(self) -> None:
        # Get the method being tested
        test_method = self.get_test_method()

        # If the method is marked with skip, skip it
        skip_reason = get_skip_reason(test_method)
        if skip_reason is not None:
            self.skipTest(skip_reason)

    @classmethod
    def instantiate_subject(cls, *args, **kwargs) -> Any:
        """
        Instantiates a test subject with the given arguments.
        """
        return cls.subject_type()(*args, **kwargs)

    def subject(self) -> Any:
        # Get the arguments to use to create the test subject from the test method
        subject_args = get_subject_args(self.get_test_method())

        # If there are arguments, use them
        if subject_args is not None:
            return self.instantiate_subject(*subject_args[0], **subject_args[1])

        # If not, try using the default arguments
        subject_args = self.common_arguments()

        # If there are default arguments, use them
        if subject_args is not None:
            return self.instantiate_subject(*subject_args[0], **subject_args[1])

        # Otherwise use the default constructor
        return self.instantiate_subject()

    def handle_regression_results(self, results):
        """
        Handles comparing the results of a regression test to
        the results stored in the regression files.

        :param results:     The results of the regression test.
        """
        # Make sure the regression results are a named map
        if not isinstance(results, dict) or not all(isinstance(key, str) for key in results):
            self.fail("Regression test '" +
                      self.get_test_method_name() +
                      "' didn't return a named map of regression results")

        # Handle each result individually
        for name, result in results.items():
            self.handle_regression_result(name, result)

    def handle_regression_result(self, name: str, result: Any):
        """
        Handles the comparison of an individual regression result
        to the stored reference.

        :param name:    The name of the regression to use.
        :param result:  The result of the regression test.
        """
        # Treat each individual regression result as a sub-test
        with self.subTest(regression=name):
            # Get the serialisers for this test method
            serialisers = self.get_serialisers()

            # Get the preference order of serialiser types
            mro = inspect.getmro(type(result))

            # Get a serialiser for this result type
            serialiser = None
            for result_type in mro:
                if result_type in serialisers:
                    serialiser = serialisers[result_type]
                    break

            # Make sure we have a serialiser
            if serialiser is None:
                self.fail("No regression serialiser found for result of type: " + type(result).__name__)

            # Complete the file path for this result
            filename: str = os.path.join(self.get_regression_path(), self.get_test_method_name(), name)

            # If the regression file doesn't exist yet, create it
            if not serialiser.exists(filename):
                serialiser.save(result, filename)

            # Otherwise load and check the saved result
            else:
                reference = serialiser.load(filename)

                # Use the serialiser's notion of equality
                failure_message: str = serialiser.compare(result, reference)
                if failure_message is not None:
                    self.fail(serialiser.__name__ + ": " + failure_message)

    @classmethod
    def get_regression_root_path(cls) -> str:
        """
        Gets the path to the root directory to store regression results in.
        Can be overridden for a custom directory per test-class.

        :return:    The path to the root directory.
        """
        return DEFAULT_REGRESSION_ROOT

    @classmethod
    def get_regression_path(cls) -> str:
        """
        Gets the path to the regression results for this test-class.

        :return:    The path.
        """
        return os.path.join(cls.get_regression_root_path(), cls.get_relative_regression_path())

    @classmethod
    def get_relative_regression_path(cls) -> str:
        """
        Gets the path to the regression results for this test-class,
        relative to the root path.

        :return:    The path.
        """
        # Get the fully-qualified name of the subject (in dotted form)
        fully_qualified_name: str = cls.subject_type().__module__ + '.' + cls.subject_type().__qualname__

        # Replace the dots with platform-dependent slashes
        return fully_qualified_name.replace(".", os.sep)

    def get_serialisers(self) -> Dict[Type, Type[RegressionSerialiser]]:
        """
        Gets the serialisers to use for this test method.

        :return:    The map from type to serialiser.
        """
        # Define the basic serialisers
        serialisers = {
            str: StringSerialiser,
            bytes: BytesSerialiser
        }

        # Add the common serialisers for this class
        common_serialisers = self.common_serialisers()
        if common_serialisers is not None:
            serialisers.update(common_serialisers)

        # Override with any test-specific serialisers
        serialisers.update(get_serialisers(self.get_test_method()))

        return serialisers

    def get_test_method(self):
        """
        Gets the test method being tested by this test instance.

        :return:    The test method.
        """
        return getattr(self, self.get_test_method_name())

    def get_test_method_name(self) -> str:
        """
        Gets the name of the test method being tested by this test instance.

        :return:    The name of the test method.
        """
        return self._testMethodName

# The default location to store regression test results # Get the method this instance is testing # If it's a marked test, it may have been given a # prefix, so substitute the base method's name Defines the subject callable for this test case. Loads the common resources for all tests in this class.         By default there are no common resources. Defines additional common serialisers to use for regression         tests in this class (in addition to the basic string and bytes         serialisers, although these can be overridden). Defines the default arguments to pass to the subject on instantiation.         By default, no arguments are passed. # Get the method being tested # If the method is marked with skip, skip it Instantiates a test subject with the given arguments. # Get the arguments to use to create the test subject from the test method # If there are arguments, use them # If not, try using the default arguments # If there are default arguments, use them # Otherwise use the default constructor Handles comparing the results of a regression test to         the results stored in the regression files.          :param results:     The results of the regression test. # Make sure the regression results are a named map # Handle each result individually Handles the comparison of an individual regression result         to the stored reference.          :param name:    The name of the regression to use.         :param result:  The result of the regression test. # Treat each individual regression result as a sub-test # Get the serialisers for this test method # Get the preference order of serialiser types # Get a serialiser for this result type # Make sure we have a serialiser # Complete the file path for this result # If the regression file doesn't exist yet, create it # Otherwise load and check the saved result # Use the serialiser's notion of equality Gets the path to the root directory to store regression results in.         Can be overridden for a custom directory per test-class.          :return:    The path to the root directory. Gets the path to the regression results for this test-class.          :return:    The path. Gets the path to the regression results for this test-class,         relative to the root path.          :return:    The path. # Get the fully-qualified name of the subject (in dotted form) # Replace the dots with platform-dependent slashes Gets the serialisers to use for this test method.          :return:    The map from type to serialiser. # Define the basic serialisers # Add the common serialisers for this class # Override with any test-specific serialisers Gets the test method being tested by this test instance.          :return:    The test method. Gets the name of the test method being tested by this test instance.          :return:    The name of the test method.