import pytest
from snakeoil import obj

# sorry, but the name is good, just too long for these tests
make_DI = obj.DelayedInstantiation
make_DIkls = obj.DelayedInstantiation_kls


class TestDelayedInstantiation:
    def test_simple(self):
        t = tuple([1, 2, 3])
        o = make_DI(tuple, lambda: t)
        assert str(t) == str(o)
        assert repr(t) == repr(o)
        assert hash(t) == hash(o)
        assert t <= o
        assert t == o
        assert t >= o

    def test_descriptor_awareness(self):
        def assertKls(
            cls,
            ignores=(),
            default_ignores=(
                "__new__",
                "__init__",
                "__init_subclass__",
                "__getattribute__",
                "__class__",
                "__getnewargs__",
                "__getstate__",
                "__doc__",
                "__class_getitem__",
            ),
        ):
            required = set(
                x for x in dir(cls) if x.startswith("__") and x.endswith("__")
            )
            missing = required.difference(obj.kls_descriptors)
            missing.difference_update(obj.base_kls_descriptors)
            missing.difference_update(default_ignores)
            missing.difference_update(ignores)
            assert (
                not missing
            ), "object %r potentially has unsupported special " "attributes: %s" % (
                cls,
                ", ".join(missing),
            )

        assertKls(object)
        assertKls(1)
        assertKls(object())
        assertKls(list)
        assertKls({})
        assertKls(set())

    def test_BaseDelayedObject(self):
        # assert that all methods/descriptors of object
        # are covered via the base.
        o = set(dir(object)).difference(
            f"__{x}__"
            for x in (
                "class",
                "getattribute",
                "new",
                "init",
                "init_subclass",
                "getstate",
                "doc",
            )
        )
        diff = o.difference(obj.base_kls_descriptors)
        assert not diff, (
            "base delayed instantiation class should cover all of object, but "
            "%r was spotted" % (",".join(sorted(diff)),)
        )
        assert obj.DelayedInstantiation_kls(int, "1") + 2 == 3

    def test_klass_choice_optimization(self):
        """ensure that BaseDelayedObject is used whenever possible"""

        # note object is an odd one- it actually has a __doc__, thus
        # it must always be a custom
        o = make_DI(object, object)
        assert object.__getattribute__(o, "__class__") is not obj.BaseDelayedObject

        class foon:
            pass

        o = make_DI(foon, foon)
        cls = object.__getattribute__(o, "__class__")
        assert cls is obj.BaseDelayedObject

        # now ensure we always get the same kls back for derivatives
        class foon:
            def __bool__(self):
                return True

        o = make_DI(foon, foon)
        cls = object.__getattribute__(o, "__class__")
        assert cls is not obj.BaseDelayedObject
        o = make_DI(foon, foon)
        cls2 = object.__getattribute__(o, "__class__")
        assert cls is cls2

    def test__class__(self):
        l = []

        def f():
            l.append(False)
            return True

        o = make_DI(bool, f)
        assert isinstance(o, bool)
        assert not l, "accessing __class__ shouldn't trigger instantiation"

    def test__doc__(self):
        l = []

        def f():
            l.append(True)
            return foon()

        class foon:
            __doc__ = "monkey"

        o = make_DI(foon, f)
        assert o.__doc__ == "monkey"
        assert not l, (
            "in accessing __doc__, the instance was generated- "
            "this is a class level attribute, thus shouldn't "
            "trigger instantiation"
        )


class TestPopattr:
    class Object:
        pass

    def setup_method(self, method):
        self.o = self.Object()
        self.o.test = 1

    def test_no_attrs(self):
        # object without any attrs
        with pytest.raises(AttributeError):
            obj.popattr(object(), "nonexistent")

    def test_nonexistent_attr(self):
        # object with attr trying to get nonexistent attr
        with pytest.raises(AttributeError):
            obj.popattr(self.o, "nonexistent")

    def test_fallback(self):
        # object with attr trying to get nonexistent attr using fallback
        value = obj.popattr(self.o, "nonexistent", 2)
        assert value == 2

    def test_removed_attr(self):
        value = obj.popattr(self.o, "test")
        assert value == 1
        # verify that attr was removed from the object
        with pytest.raises(AttributeError):
            obj.popattr(self.o, "test")
