from abc import abstractmethod, ABCMeta
from unittest import TestCase

from six import with_metaclass

from catalyst.utils.final import (
    FinalMeta,
    final,
)
from catalyst.utils.metautils import compose_types


class FinalMetaTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        class ClassWithFinal(with_metaclass(FinalMeta, object)):
            a = final('ClassWithFinal: a')
            b = 'ClassWithFinal: b'

            @final
            def f(self):
                return 'ClassWithFinal: f'

            def g(self):
                return 'ClassWithFinal: g'

        cls.class_ = ClassWithFinal

    def test_subclass_no_override(self):
        """
        Tests that it is valid to create a subclass that does not override
        any methods.
        """
        class SubClass(self.class_):
            pass

    def test_subclass_no_final_override(self):
        """
        Tests that it is valid to create a subclass that does not override
        and final methods.
        """
        class SubClass(self.class_):
            b = 'SubClass: b'

            def g(self):
                return 'SubClass: g'

    def test_override_final_no_decorator(self):
        """
        Tests that attempting to create a subclass that overrides a final
        method will raise a `TypeError`.
        """
        with self.assertRaises(TypeError):
            class SubClass(self.class_):
                def f(self):
                    return 'SubClass: f'

    def test_override_final_attribute(self):
        """
        Tests that attempting to create a subclass that overrides a final
        attribute will raise a `TypeError`.
        """
        with self.assertRaises(TypeError):
            class SubClass(self.class_):
                a = 'SubClass: a'

    def test_override_final_with_decorator(self):
        """
        Tests that attempting to create a subclass that overrides a final
        method will raise a `TypeError` even if you mark the new version as
        final.
        """
        with self.assertRaises(TypeError):
            class SubClass(self.class_):
                @final
                def f(self):
                    return 'SubClass: f'

    def test_override_final_attribute_with_final(self):
        """
        Tests that attempting to create a subclass that overrides a final
        attribute will raise a `TypeError` even if you mark the new version as
        final.
        """
        with self.assertRaises(TypeError):
            class SubClass(self.class_):
                a = final('SubClass: a')

    def test_override_on_class_object(self):
        """
        Tests overriding final methods and attributes on the class object
        itself.
        """
        class SubClass(self.class_):
            pass

        with self.assertRaises(TypeError):
            SubClass.f = lambda self: 'SubClass: f'

        with self.assertRaises(TypeError):
            SubClass.a = 'SubClass: a'

    def test_override_on_instance(self):
        """
        Tests overriding final methods on instances of a class.
        """
        class SubClass(self.class_):
            def h(self):
                pass

        s = SubClass()
        with self.assertRaises(TypeError):
            s.f = lambda self: 'SubClass: f'

        with self.assertRaises(TypeError):
            s.a = lambda self: 'SubClass: a'

    def test_override_on_super(self):
        """
        Tests overriding on the class that has the @final methods in it.
        """
        old_a = self.class_.a
        old_f = self.class_.f
        try:
            with self.assertRaises(TypeError):
                self.class_.f = lambda *args: None
        except Exception:
            self.class_.f = old_f
            raise

        try:
            with self.assertRaises(TypeError):
                self.class_.a = 'SubClass: a'
        except Exception:
            self.class_.a = old_a
            raise

    def test_override___setattr___on_subclass(self):
        """
        Tests an attempt to override __setattr__ which is implicitly final.
        """
        with self.assertRaises(TypeError):
            class SubClass(self.class_):
                def __setattr__(self, name, value):
                    object.__setattr__(self, name, value)

    def test_override___setattr___on_instance(self):
        """
        Tests overriding __setattr__ on an instance.
        """
        class SubClass(self.class_):
            pass

        s = SubClass()
        with self.assertRaises(TypeError):
            s.__setattr__ = lambda a, b: None


class FinalABCMetaTestCase(FinalMetaTestCase):
    @classmethod
    def setUpClass(cls):
        FinalABCMeta = compose_types(FinalMeta, ABCMeta)

        class ABCWithFinal(with_metaclass(FinalABCMeta, object)):
            a = final('ABCWithFinal: a')
            b = 'ABCWithFinal: b'

            @final
            def f(self):
                return 'ABCWithFinal: f'

            def g(self):
                return 'ABCWithFinal: g'

            @abstractmethod
            def h(self):
                raise NotImplementedError('h')

        cls.class_ = ABCWithFinal

    def test_cannot_instantiate_subclass(self):
        """
        Tests that you cannot create an instance of a subclass
        that does not implement the abstractmethod h.
        """
        class AbstractSubClass(self.class_):
            pass

        with self.assertRaises(TypeError):
            AbstractSubClass()

    def test_override_on_instance(self):
        class SubClass(self.class_):
            def h(self):
                """
                Pass the abstract tests by creating this method.
                """
                pass

        s = SubClass()
        with self.assertRaises(TypeError):
            s.f = lambda self: 'SubClass: f'

    def test_override___setattr___on_instance(self):
        """
        Tests overriding __setattr__ on an instance.
        """
        class SubClass(self.class_):
            def h(self):
                pass

        s = SubClass()
        with self.assertRaises(TypeError):
            s.__setattr__ = lambda a, b: None

    def test_subclass_setattr(self):
        """
        Tests that subclasses don't destroy the __setattr__.
        """
        class ClassWithFinal(with_metaclass(FinalMeta, object)):
            @final
            def f(self):
                return 'ClassWithFinal: f'

        class SubClass(ClassWithFinal):
            def __init__(self):
                self.a = 'a'

        SubClass()
        self.assertEqual(SubClass().a, 'a')
        self.assertEqual(SubClass().f(), 'ClassWithFinal: f')

    def test_final_classmethod(self):

        class ClassWithClassMethod(with_metaclass(FinalMeta, object)):
            count = 0

            @final
            @classmethod
            def f(cls):
                cls.count += 1
                return cls.count

        with self.assertRaises(TypeError):
            class ClassOverridingClassMethod(ClassWithClassMethod):
                @classmethod
                def f(cls):
                    return "Oh Noes!"

        with self.assertRaises(TypeError):
            ClassWithClassMethod.f = lambda cls: 0

        self.assertEqual(ClassWithClassMethod.f(), 1)
        self.assertEqual(ClassWithClassMethod.f(), 2)
        self.assertEqual(ClassWithClassMethod.f(), 3)

        instance = ClassWithClassMethod()

        with self.assertRaises(TypeError):
            instance.f = lambda cls: 0

        self.assertEqual(ClassWithClassMethod.f(), 4)
        self.assertEqual(ClassWithClassMethod.f(), 5)
        self.assertEqual(ClassWithClassMethod.f(), 6)
