import math
import re
from functools import partial
from time import time

import pytest
from snakeoil import klass


class Test_GetAttrProxy:
    kls = staticmethod(klass.GetAttrProxy)

    def test_it(self):
        class foo1:
            def __init__(self, obj):
                self.obj = obj

            __getattr__ = self.kls("obj")

        class foo2:
            pass

        o2 = foo2()
        o = foo1(o2)
        with pytest.raises(AttributeError):
            getattr(o, "blah")
        assert o.obj == o2
        o2.foon = "dar"
        assert o.foon == "dar"
        o.foon = "foo"
        assert o.foon == "foo"

    def test_attrlist(self):
        def make_class(attr_list=None):
            class foo(metaclass=self.kls):
                if attr_list is not None:
                    locals()["__attr_comparison__"] = attr_list

        with pytest.raises(TypeError):
            make_class()
        with pytest.raises(TypeError):
            make_class(["foon"])
        with pytest.raises(TypeError):
            make_class([None])

    def test_instancemethod(self):
        class foo:
            bar = "baz"

        class Test:
            method = self.kls("test")
            test = foo()

        test = Test()
        assert test.method("bar") == foo.bar


class TestDirProxy:
    @staticmethod
    def noninternal_attrs(obj):
        return sorted(x for x in dir(obj) if not re.match(r"__\w+__", x))

    def test_combined(self):
        class foo1:
            def __init__(self, obj):
                self.obj = obj

            __dir__ = klass.DirProxy("obj")

        class foo2:
            def __init__(self):
                self.attr = "foo"

        o2 = foo2()
        o = foo1(o2)
        assert self.noninternal_attrs(o) == ["attr", "obj"]

    def test_empty(self):
        class foo1:
            def __init__(self, obj):
                self.obj = obj

            __dir__ = klass.DirProxy("obj")

        class foo2:
            pass

        o2 = foo2()
        o = foo1(o2)
        assert self.noninternal_attrs(o2) == []
        assert self.noninternal_attrs(o) == ["obj"]

    def test_slots(self):
        class foo1:
            __slots__ = ("obj",)

            def __init__(self, obj):
                self.obj = obj

            __dir__ = klass.DirProxy("obj")

        class foo2:
            __slots__ = ("attr",)

            def __init__(self):
                self.attr = "foo"

        o2 = foo2()
        o = foo1(o2)
        assert self.noninternal_attrs(o) == ["attr", "obj"]


class Test_contains:
    func = staticmethod(klass.contains)

    def test_it(self):
        class c(dict):
            __contains__ = self.func

        d = c({"1": 2})
        assert "1" in d
        assert 1 not in d


class Test_get:
    func = staticmethod(klass.get)

    def test_it(self):
        class c(dict):
            get = self.func

        d = c({"1": 2})
        assert d.get("1") == 2
        assert d.get("1", 3) == 2
        assert d.get(1) is None
        assert d.get(1, 3) == 3


class Test_chained_getter:
    kls = klass.chained_getter

    def test_hash(self):
        assert hash(self.kls("foon")) == hash("foon")
        assert hash(self.kls("foon.dar")) == hash("foon.dar")

    def test_caching(self):
        l = [id(self.kls("fa2341f%s" % x)) for x in "abcdefghij"]
        assert id(self.kls("fa2341fa")) == l[0]

    def test_eq(self):
        assert self.kls("asdf", disable_inst_caching=True) == self.kls(
            "asdf", disable_inst_caching=True
        )

        assert self.kls("asdf2", disable_inst_caching=True) != self.kls(
            "asdf", disable_inst_caching=True
        )

    def test_it(self):
        class maze:
            def __init__(self, kwargs):
                self.__data__ = kwargs

            def __getattr__(self, attr):
                return self.__data__.get(attr, self)

        d = {}
        m = maze(d)
        f = self.kls
        assert f("foon")(m) == m
        d["foon"] = 1
        assert f("foon")(m) == 1
        assert f("dar.foon")(m) == 1
        assert f(".".join(["blah"] * 10))(m) == m
        with pytest.raises(AttributeError):
            f("foon.dar")(m)


class Test_jit_attr:
    kls = staticmethod(klass._internal_jit_attr)

    @property
    def jit_attr(self):
        return partial(klass.jit_attr, kls=self.kls)

    @property
    def jit_attr_named(self):
        return partial(klass.jit_attr_named, kls=self.kls)

    @property
    def jit_attr_ext_method(self):
        return partial(klass.jit_attr_ext_method, kls=self.kls)

    def mk_inst(
        self,
        attrname="_attr",
        method_lookup=False,
        use_cls_setattr=False,
        func=None,
        singleton=klass._uncached_singleton,
    ):
        f = func
        if not func:

            def f(self):
                self._invokes.append(self)
                return 54321

        class cls:
            def __init__(self):
                sf = partial(object.__setattr__, self)
                sf("_sets", [])
                sf("_reflects", [])
                sf("_invokes", [])

            attr = self.kls(f, attrname, singleton, use_cls_setattr)

            def __setattr__(self, attr, value):
                self._sets.append(self)
                object.__setattr__(self, attr, value)

            def reflect(self):
                self._reflects.append(self)
                return 12345

        return cls()

    def assertState(self, instance, sets=0, reflects=0, invokes=0, value=54321):
        assert instance.attr == value
        sets = [instance] * sets
        reflects = [instance] * reflects
        invokes = [instance] * invokes
        msg = (
            "checking %s: got(%r), expected(%r); state was sets=%r, "
            "reflects=%r, invokes=%r"
            % ("%s", "%s", "%s", instance._sets, instance._reflects, instance._invokes)
        )
        assert instance._sets == sets, msg % ("sets", instance._sets, sets)
        assert instance._reflects == reflects, msg % (
            "reflects",
            instance._reflects,
            reflects,
        )
        assert instance._invokes == invokes, msg % (
            "invokes",
            instance._invokes,
            invokes,
        )

    def test_implementation(self):
        obj = self.mk_inst()

        # default state is use_cls_setattr = False
        self.assertState(obj, invokes=1)
        self.assertState(obj, invokes=1)
        del obj._attr
        self.assertState(obj, invokes=2)
        self.assertState(obj, invokes=2)

        # basic caching is now verified.
        obj = self.mk_inst(use_cls_setattr=True)
        self.assertState(obj, sets=1, invokes=1)
        self.assertState(obj, sets=1, invokes=1)
        del obj._attr
        self.assertState(obj, sets=2, invokes=2)
        self.assertState(obj, sets=2, invokes=2)

    def test_jit_attr(self):
        now = time()

        class cls:
            @self.jit_attr
            def my_attr(self):
                return now

        o = cls()
        assert o.my_attr == now
        assert o._my_attr == now

        class cls:
            @self.jit_attr
            def attr2(self):
                return now

            def __setattr__(self, attr, value):
                raise AssertionError("setattr was invoked")

        o = cls()
        assert o.attr2 == now
        assert o._attr2 == now
        del o._attr2
        assert o.attr2 == now
        assert o._attr2 == now

    def test_jit_attr_named(self):
        now = time()

        # check attrname control and default object.__setattr__ avoidance
        class cls:
            @self.jit_attr_named("_blah")
            def my_attr(self):
                return now

            def __setattr__(self, attr, value):
                raise AssertionError("setattr was invoked")

        o = cls()
        assert o.my_attr == now
        assert o._blah == now

        class cls:
            @self.jit_attr_named("_blah2", use_cls_setattr=True)
            def my_attr(self):
                return now

            def __setattr__(self, attr, value):
                object.__setattr__(self, "invoked", True)
                object.__setattr__(self, attr, value)

        o = cls()
        assert not hasattr(o, "invoked")
        assert o.my_attr == now
        assert o._blah2 == now
        assert o.invoked

    def test_jit_attr_ext_method(self):
        now = time()
        now2 = now + 100

        class base:
            def f1(self):
                return now

            def f2(self):
                return now2

            def __setattr__(self, attr, value):
                if not getattr(self, "_setattr_allowed", False):
                    raise TypeError("setattr isn't allowed for %s" % attr)
                object.__setattr__(self, attr, value)

        base.attr = self.jit_attr_ext_method("f1", "_attr")
        o = base()
        assert o.attr == now
        assert o._attr == now
        assert o.attr == now

        base.attr = self.jit_attr_ext_method("f1", "_attr", use_cls_setattr=True)
        o = base()
        with pytest.raises(TypeError):
            getattr(o, "attr")
        base._setattr_allowed = True
        assert o.attr == now

        base.attr = self.jit_attr_ext_method("f2", "_attr2")
        o = base()
        assert o.attr == now2
        assert o._attr2 == now2

        # finally, check that it's doing lookups rather then storing the func.
        base.attr = self.jit_attr_ext_method("func", "_attr2")
        o = base()
        # no func...
        with pytest.raises(AttributeError):
            getattr(o, "attr")
        base.func = base.f1
        assert o.attr == now
        assert o._attr2 == now
        # check caching...
        base.func = base.f2
        assert o.attr == now
        del o._attr2
        assert o.attr == now2

    def test_check_singleton_is_compare(self):
        def throw_assert(*args, **kwds):
            raise AssertionError(
                "I shouldn't be invoked: %s, %s"
                % (
                    args,
                    kwds,
                )
            )

        class puker:
            __eq__ = throw_assert

        puker_singleton = puker()

        obj = self.mk_inst(singleton=puker_singleton)
        obj._attr = puker_singleton
        # force attr access. if it's done wrong, it'll puke.
        # pylint: disable=pointless-statement
        obj.attr

    def test_cached_property(self):
        l = []

        class foo:
            @klass.cached_property
            def blah(self, l=l, i=iter(range(5))):
                l.append(None)
                return next(i)

        f = foo()
        assert f.blah == 0
        assert len(l) == 1
        assert f.blah == 0
        assert len(l) == 1
        del f.blah
        assert f.blah == 1
        assert len(l) == 2

    def test_cached_property(self):
        l = []

        def named(self, l=l, i=iter(range(5))):
            l.append(None)
            return next(i)

        class foo:
            blah = klass.cached_property_named("blah")(named)

        f = foo()
        assert f.blah == 0
        assert len(l) == 1
        assert f.blah == 0
        assert len(l) == 1
        del f.blah
        assert f.blah == 1
        assert len(l) == 2


class Test_aliased_attr:
    func = staticmethod(klass.alias_attr)

    def test_it(self):
        class cls:
            attr = self.func("dar.blah")

        o = cls()
        with pytest.raises(AttributeError):
            getattr(o, "attr")
        o.dar = "foon"

        with pytest.raises(AttributeError):
            getattr(o, "attr")
        o.dar = o
        o.blah = "monkey"

        assert o.attr == "monkey"

        # verify it'll cross properties...
        class blah:
            target = object()

        class cls:
            @property
            def foon(self):
                return blah()

            alias = self.func("foon.target")

        o = cls()
        assert o.alias is blah.target


class Test_cached_hash:
    func = staticmethod(klass.cached_hash)

    def test_it(self):
        now = int(time())

        class cls:
            invoked = []

            @self.func
            def __hash__(self):
                self.invoked.append(self)
                return now

        o = cls()
        assert hash(o) == now
        assert o.invoked == [o]
        # ensure it cached...
        assert hash(o) == now
        assert o.invoked == [o]
        assert o._hash == now


class Test_reflective_hash:
    func = staticmethod(klass.reflective_hash)

    def test_it(self):
        class cls:
            __hash__ = self.func("_hash")

        obj = cls()
        with pytest.raises(AttributeError):
            hash(obj)
        obj._hash = 1
        assert hash(obj) == 1
        obj._hash = 123123123
        assert hash(obj) == 123123123
        # verify it's not caching in any form
        del obj._hash
        with pytest.raises(AttributeError):
            hash(obj)

        class cls2:
            __hash__ = self.func("_dar")

        obj = cls2()
        with pytest.raises(AttributeError):
            hash(obj)
        obj._dar = 4
        assert hash(obj) == 4


class TestImmutableInstance:
    def test_metaclass(self):
        self.common_test(lambda x: x, metaclass=klass.immutable_instance)

    def test_injection(self):
        def f(scope):
            klass.inject_immutable_instance(scope)

        self.common_test(f)

    def common_test(self, modify_kls, **kwargs):
        class kls(**kwargs):
            modify_kls(locals())

        o = kls()
        with pytest.raises(AttributeError):
            setattr(o, "dar", "foon")
        with pytest.raises(AttributeError):
            delattr(o, "dar")

        object.__setattr__(o, "dar", "foon")
        with pytest.raises(AttributeError):
            delattr(o, "dar")

        # ensure it only sets it if nothing is in place already.

        class kls(**kwargs):
            def __setattr__(self, attr, value):
                raise TypeError(self)

            modify_kls(locals())

        o = kls()
        with pytest.raises(TypeError):
            setattr(o, "dar", "foon")
        with pytest.raises(AttributeError):
            delattr(o, "dar")


class TestAliasMethod:
    func = staticmethod(klass.alias_method)

    def test_alias_method(self):
        class kls:
            __len__ = lambda s: 3
            lfunc = self.func("__len__")

        c = kls()
        assert c.__len__() == c.lfunc()
        c.__len__ = lambda: 4
        assert c.__len__() == c.lfunc()


class TestPatch:
    def setup_method(self, method):
        # cache original methods
        self._math_ceil = math.ceil
        self._math_floor = math.floor

    def teardown_method(self, method):
        # restore original methods
        math.ceil = self._math_ceil
        math.floor = self._math_floor

    def test_patch(self):
        n = 0.1
        assert math.ceil(n) == 1

        @klass.patch("math.ceil")
        def ceil(orig_ceil, n):
            return math.floor(n)

        assert math.ceil(n) == 0

    def test_multiple_patches(self):
        n = 1.1
        assert math.ceil(n) == 2
        assert math.floor(n) == 1

        @klass.patch("math.ceil")
        @klass.patch("math.floor")
        def zero(orig_func, n):
            return 0

        assert math.ceil(n) == 0
        assert math.floor(n) == 0
