##############################################################################
# Copyright (c) 2020 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
##############################################################################

import unittest

from zope.interface.common import ABCInterface
from zope.interface.common import ABCInterfaceClass
from zope.interface.verify import verifyClass
from zope.interface.verify import verifyObject


def iter_abc_interfaces(predicate=lambda iface: True):
    # Iterate ``(iface, classes)``, where ``iface`` is a descendent of
    # the ABCInterfaceClass passing the *predicate* and ``classes`` is
    # an iterable of classes registered to conform to that interface.
    #
    # Note that some builtin classes are registered for two distinct parts of
    # the ABC/interface tree. For example, bytearray is both ByteString and
    # MutableSequence.
    seen = set()
    stack = list(
        ABCInterface.dependents
    )  # subclasses, but also implementedBy objects

    while stack:
        iface = stack.pop(0)
        if iface in seen or not isinstance(iface, ABCInterfaceClass):
            continue
        seen.add(iface)
        stack.extend(list(iface.dependents))
        if not predicate(iface):
            continue

        registered = set(iface.getRegisteredConformers())
        registered -= set(iface._ABCInterfaceClass__ignored_classes)
        if registered:
            yield iface, registered


def add_abc_interface_tests(cls, module):
    def predicate(iface):
        return iface.__module__ == module
    add_verify_tests(cls, iter_abc_interfaces(predicate))


def add_verify_tests(cls, iface_classes_iter):
    cls.maxDiff = None
    for iface, registered_classes in iface_classes_iter:
        for stdlib_class in registered_classes:
            def test(self, stdlib_class=stdlib_class, iface=iface):
                if (
                    stdlib_class in self.UNVERIFIABLE or
                    stdlib_class.__name__ in self.UNVERIFIABLE
                ):
                    self.skipTest("Unable to verify %s" % stdlib_class)

                self.assertTrue(self.verify(iface, stdlib_class))

            suffix = "{}_{}_{}_{}".format(
                stdlib_class.__module__.replace('.', '_'),
                stdlib_class.__name__,
                iface.__module__.replace('.', '_'),
                iface.__name__
            )
            name = 'test_auto_' + suffix
            test.__name__ = name
            assert not hasattr(cls, name), (name, list(cls.__dict__))
            setattr(cls, name, test)

            def test_ro(self, stdlib_class=stdlib_class, iface=iface):
                from zope.interface import Interface
                from zope.interface import implementedBy
                from zope.interface import ro
                self.assertEqual(
                    tuple(ro.ro(iface, strict=True)),
                    iface.__sro__)
                implements = implementedBy(stdlib_class)
                sro = implements.__sro__
                self.assertIs(sro[-1], Interface)

                if stdlib_class not in self.UNVERIFIABLE_RO:
                    # Check that we got the strict C3 resolution order, unless
                    # we know we cannot. Note that 'Interface' is virtual base
                    # that doesn't necessarily appear at the same place in the
                    # calculated SRO as in the final SRO.
                    strict = stdlib_class not in self.NON_STRICT_RO
                    isro = ro.ro(implements, strict=strict)
                    isro.remove(Interface)
                    isro.append(Interface)

                    self.assertEqual(tuple(isro), sro)

            name = 'test_auto_ro_' + suffix
            test_ro.__name__ = name
            assert not hasattr(cls, name)
            setattr(cls, name, test_ro)


class VerifyClassMixin(unittest.TestCase):
    verifier = staticmethod(verifyClass)
    UNVERIFIABLE = ()
    NON_STRICT_RO = ()
    UNVERIFIABLE_RO = ()

    def _adjust_object_before_verify(self, iface, x):
        return x

    def verify(self, iface, klass, **kwargs):
        return self.verifier(iface,
                             self._adjust_object_before_verify(iface, klass),
                             **kwargs)


class VerifyObjectMixin(VerifyClassMixin):
    verifier = staticmethod(verifyObject)
    CONSTRUCTORS = {
    }

    def _adjust_object_before_verify(self, iface, x):
        constructor = self.CONSTRUCTORS.get(x)
        if not constructor:
            constructor = self.CONSTRUCTORS.get(iface)
        if not constructor:
            constructor = self.CONSTRUCTORS.get(x.__name__)
        if not constructor:
            constructor = x
        if constructor is unittest.SkipTest:
            self.skipTest("Cannot create " + str(x))

        try:
            result = constructor()
        except Exception as e:  # pragma: no cover
            raise TypeError(
                f'Failed to create instance of {constructor}') from e
        if hasattr(result, 'close'):
            self.addCleanup(result.close)
        return result
