import asyncio
import copy
import math
import operator
import time
from contextvars import ContextVar
from threading import Thread

import pytest

from werkzeug import local

# Since the tests are creating local instances, use global context vars
# to avoid accumulating anonymous context vars that can't be collected.
_cv_ns = ContextVar("werkzeug.tests.ns")
_cv_stack = ContextVar("werkzeug.tests.stack")
_cv_val = ContextVar("werkzeug.tests.val")


@pytest.fixture(autouse=True)
def reset_context_vars():
    ns_token = _cv_ns.set({})
    stack_token = _cv_stack.set([])
    yield
    _cv_ns.reset(ns_token)
    _cv_stack.reset(stack_token)


def test_basic_local():
    ns = local.Local(_cv_ns)
    ns.foo = 0
    values = []

    def value_setter(idx):
        time.sleep(0.01 * idx)
        ns.foo = idx
        time.sleep(0.02)
        values.append(ns.foo)

    threads = [Thread(target=value_setter, args=(x,)) for x in [1, 2, 3]]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    assert sorted(values) == [1, 2, 3]

    def delfoo():
        del ns.foo

    delfoo()
    pytest.raises(AttributeError, lambda: ns.foo)
    pytest.raises(AttributeError, delfoo)

    local.release_local(ns)


def test_basic_local_asyncio():
    ns = local.Local(_cv_ns)
    ns.foo = 0
    values = []

    async def value_setter(idx):
        await asyncio.sleep(0.01 * idx)
        ns.foo = idx
        await asyncio.sleep(0.02)
        values.append(ns.foo)

    async def main():
        futures = [asyncio.ensure_future(value_setter(i)) for i in [1, 2, 3]]
        await asyncio.gather(*futures)

    asyncio.run(main())
    assert sorted(values) == [1, 2, 3]

    def delfoo():
        del ns.foo

    delfoo()
    pytest.raises(AttributeError, lambda: ns.foo)
    pytest.raises(AttributeError, delfoo)

    local.release_local(ns)


def test_local_release():
    ns = local.Local(_cv_ns)
    ns.foo = 42
    local.release_local(ns)
    assert not hasattr(ns, "foo")

    ls = local.LocalStack(_cv_stack)
    ls.push(42)
    local.release_local(ls)
    assert ls.top is None


def test_local_stack():
    ls = local.LocalStack(_cv_stack)
    assert ls.top is None
    ls.push(42)
    assert ls.top == 42
    ls.push(23)
    assert ls.top == 23
    ls.pop()
    assert ls.top == 42
    ls.pop()
    assert ls.top is None
    assert ls.pop() is None
    assert ls.pop() is None

    proxy = ls()
    ls.push([1, 2])
    assert proxy == [1, 2]
    ls.push((1, 2))
    assert proxy == (1, 2)
    ls.pop()
    ls.pop()
    assert repr(proxy) == "<LocalProxy unbound>"


def test_local_stack_asyncio():
    ls = local.LocalStack(_cv_stack)
    ls.push(1)

    async def task():
        ls.push(1)
        assert len(ls._storage.get()) == 2

    async def main():
        futures = [asyncio.ensure_future(task()) for _ in range(3)]
        await asyncio.gather(*futures)

    asyncio.run(main())


def test_proxy_local():
    ns = local.Local(_cv_ns)
    ns.foo = []
    p = local.LocalProxy(ns, "foo")
    p.append(42)
    p.append(23)
    p[1:] = [1, 2, 3]
    assert p == [42, 1, 2, 3]
    assert p == ns.foo
    ns.foo += [1]
    assert list(p) == [42, 1, 2, 3, 1]
    p_from_local = ns("foo")
    p_from_local.append(2)
    assert p == p_from_local
    assert p._get_current_object() is ns.foo


def test_proxy_callable():
    value = 42
    p = local.LocalProxy(lambda: value)
    assert p == 42
    value = [23]
    p.append(42)
    assert p == [23, 42]
    assert value == [23, 42]
    assert p._get_current_object() is value


def test_proxy_wrapped():
    class SomeClassWithWrapped:
        __wrapped__ = "wrapped"

    proxy = local.LocalProxy(_cv_val)
    assert proxy.__wrapped__ is _cv_val
    _cv_val.set(42)

    with pytest.raises(AttributeError):
        proxy.__wrapped__  # noqa: B018

    ns = local.Local(_cv_ns)
    ns.foo = SomeClassWithWrapped()
    ns.bar = 42

    assert ns("foo").__wrapped__ == "wrapped"

    with pytest.raises(AttributeError):
        ns("bar").__wrapped__  # noqa: B018


def test_proxy_doc():
    def example():
        """example doc"""

    assert local.LocalProxy(lambda: example).__doc__ == "example doc"
    # The __doc__ descriptor shouldn't block the LocalProxy's class doc.
    assert local.LocalProxy.__doc__.startswith("A proxy")


def test_proxy_fallback():
    local_stack = local.LocalStack(_cv_stack)
    local_proxy = local_stack()

    assert repr(local_proxy) == "<LocalProxy unbound>"
    assert isinstance(local_proxy, local.LocalProxy)
    assert local_proxy.__class__ is local.LocalProxy
    assert "LocalProxy" in local_proxy.__doc__

    local_stack.push(42)

    assert repr(local_proxy) == "42"
    assert isinstance(local_proxy, int)
    assert local_proxy.__class__ is int
    assert "int(" in local_proxy.__doc__


def test_proxy_unbound():
    ns = local.Local(_cv_ns)
    p = ns("value")
    assert repr(p) == "<LocalProxy unbound>"
    assert not p
    assert dir(p) == []


def _make_proxy(value):
    ns = local.Local(_cv_ns)
    ns.value = value
    p = ns("value")
    return ns, p


def test_proxy_type():
    _, p = _make_proxy([])
    assert isinstance(p, list)
    assert p.__class__ is list
    assert issubclass(type(p), local.LocalProxy)
    assert type(p) is local.LocalProxy


def test_proxy_string_representations():
    class Example:
        def __repr__(self):
            return "a"

        def __bytes__(self):
            return b"b"

        def __index__(self):
            return 23

    _, p = _make_proxy(Example())
    assert str(p) == "a"
    assert repr(p) == "a"
    assert bytes(p) == b"b"
    # __index__
    assert bin(p) == "0b10111"
    assert oct(p) == "0o27"
    assert hex(p) == "0x17"


def test_proxy_hash():
    ns, p = _make_proxy("abc")
    assert hash(ns.value) == hash(p)


@pytest.mark.parametrize(
    "op",
    [
        operator.lt,
        operator.le,
        operator.eq,
        operator.ne,
        operator.gt,
        operator.ge,
        operator.add,
        operator.sub,
        operator.mul,
        operator.truediv,
        operator.floordiv,
        operator.mod,
        divmod,
        pow,
        operator.lshift,
        operator.rshift,
        operator.and_,
        operator.or_,
        operator.xor,
    ],
)
def test_proxy_binop_int(op):
    _, p = _make_proxy(2)
    assert op(p, 3) == op(2, 3)
    # r-op
    assert op(3, p) == op(3, 2)


@pytest.mark.parametrize("op", [operator.neg, operator.pos, abs, operator.invert])
def test_proxy_uop_int(op):
    _, p = _make_proxy(-2)
    assert op(p) == op(-2)


def test_proxy_numeric():
    class Example:
        def __complex__(self):
            return 1 + 2j

        def __int__(self):
            return 1

        def __float__(self):
            return 2.1

        def __round__(self, n=None):
            if n is not None:
                return 3.3

            return 3

        def __trunc__(self):
            return 4

        def __floor__(self):
            return 5

        def __ceil__(self):
            return 6

        def __index__(self):
            return 2

    _, p = _make_proxy(Example())
    assert complex(p) == 1 + 2j
    assert int(p) == 1
    assert float(p) == 2.1
    assert round(p) == 3
    assert round(p, 2) == 3.3
    assert math.trunc(p) == 4
    assert math.floor(p) == 5
    assert math.ceil(p) == 6
    assert [1, 2, 3][p] == 3  # __index__


@pytest.mark.parametrize(
    "op",
    [
        operator.iadd,
        operator.isub,
        operator.imul,
        operator.imatmul,
        operator.itruediv,
        operator.ifloordiv,
        operator.imod,
        operator.ipow,
        operator.ilshift,
        operator.irshift,
        operator.iand,
        operator.ior,
        operator.ixor,
    ],
)
def test_proxy_iop(op):
    class Example:
        value = 1

        def fake_op(self, other):
            self.value = other
            return self

        __iadd__ = fake_op
        __isub__ = fake_op
        __imul__ = fake_op
        __imatmul__ = fake_op
        __itruediv__ = fake_op
        __ifloordiv__ = fake_op
        __imod__ = fake_op
        __ipow__ = fake_op
        __ilshift__ = fake_op
        __irshift__ = fake_op
        __iand__ = fake_op
        __ior__ = fake_op
        __ixor__ = fake_op

    ns, p = _make_proxy(Example())
    p_out = op(p, 2)
    assert type(p_out) is local.LocalProxy
    assert p.value == 2
    assert ns.value.value == 2


def test_proxy_matmul():
    class Example:
        def __matmul__(self, other):
            return 2 * other

        def __rmatmul__(self, other):
            return 2 * other

    _, p = _make_proxy(Example())
    assert p @ 3 == 6
    assert 4 @ p == 8


def test_proxy_str():
    _, p = _make_proxy("{act} %s")
    assert p + " world" == "{act} %s world"
    assert "say " + p == "say {act} %s"
    assert p * 2 == "{act} %s{act} %s"
    assert 2 * p == p * 2
    assert p % ("world",) == "{act} world"
    assert p.format(act="test") == "test %s"


def test_proxy_list():
    _, p = _make_proxy([1, 2, 3])
    assert len(p) == 3
    assert p[0] == 1
    assert 3 in p
    assert 4 not in p
    assert tuple(p) == (1, 2, 3)
    assert list(reversed(p)) == [3, 2, 1]
    p[0] = 4
    assert p == [4, 2, 3]
    del p[-1]
    assert p == [4, 2]
    p += [5]
    assert p[-1] == 5
    p *= 2
    assert len(p) == 6
    p[:] = []
    assert not p
    p.append(1)
    assert p
    assert p + [2] == [1, 2]
    assert [2] + p == [2, 1]


def test_proxy_copy():
    class Foo:
        def __copy__(self):
            return self

        def __deepcopy__(self, memo):
            return self

    ns, p = _make_proxy(Foo())
    assert copy.copy(p) is ns.value
    assert copy.deepcopy(p) is ns.value

    a = []
    _, p = _make_proxy([a])
    assert copy.copy(p) == [a]
    assert copy.copy(p)[0] is a
    assert copy.deepcopy(p) == [a]
    assert copy.deepcopy(p)[0] is not a


def test_proxy_iterator():
    a = [1, 2, 3]
    _, p = _make_proxy(iter(a))
    assert next(p) == 1


def test_proxy_length_hint():
    class Example:
        def __length_hint__(self):
            return 2

    _, p = _make_proxy(Example())
    assert operator.length_hint(p) == 2


def test_proxy_context_manager():
    class Example:
        value = 2

        def __enter__(self):
            self.value += 1
            return self

        def __exit__(self, exc_type, exc_val, exc_tb):
            self.value -= 1

    _, p = _make_proxy(Example())
    assert p.value == 2

    with p:
        assert p.value == 3

    assert p.value == 2


def test_proxy_class():
    class Meta(type):
        def __instancecheck__(cls, instance):
            return True

        def __subclasscheck__(cls, subclass):
            return True

    class Parent:
        pass

    class Example(Parent, metaclass=Meta):
        pass

    class Child(Example):
        pass

    _, p = _make_proxy(Example)
    assert type(p()) is Example
    assert isinstance(1, p)
    assert issubclass(int, p)
    assert p.__mro__ == (Example, Parent, object)
    assert p.__bases__ == (Parent,)
    assert p.__subclasses__() == [Child]


def test_proxy_attributes():
    class Example:
        def __init__(self):
            object.__setattr__(self, "values", {})

        def __getattribute__(self, name):
            if name == "ham":
                return "eggs"

            return super().__getattribute__(name)

        def __getattr__(self, name):
            return self.values.get(name)

        def __setattr__(self, name, value):
            self.values[name] = value

        def __delattr__(self, name):
            del self.values[name]

        def __dir__(self):
            return sorted(self.values.keys())

    _, p = _make_proxy(Example())
    assert p.nothing is None
    assert p.__dict__ == {"values": {}}
    assert dir(p) == []

    p.x = 1
    assert p.x == 1
    assert dir(p) == ["x"]

    del p.x
    assert dir(p) == []

    assert p.ham == "eggs"
    p.ham = "spam"
    assert p.ham == "eggs"
    assert p.values["ham"] == "spam"


def test_proxy_await():
    async def get():
        return 1

    _, p = _make_proxy(get())

    async def main():
        return await p

    out = asyncio.run(main())
    assert out == 1


def test_proxy_aiter():
    class Example:
        value = 3

        def __aiter__(self):
            return self

        async def __anext__(self):
            if self.value:
                self.value -= 1
                return self.value

            raise StopAsyncIteration

    _, p = _make_proxy(Example())

    async def main():
        out = []

        async for v in p:
            out.append(v)

        return out

    out = asyncio.run(main())
    assert out == [2, 1, 0]


def test_proxy_async_context_manager():
    class Example:
        value = 2

        async def __aenter__(self):
            self.value += 1
            return self

        async def __aexit__(self, exc_type, exc_val, exc_tb):
            self.value -= 1

    _, p = _make_proxy(Example())

    async def main():
        async with p:
            assert p.value == 3

        assert p.value == 2
        return True

    assert asyncio.run(main())
