from __future__ import annotations

import errno
from unittest.mock import ANY, Mock, call, patch

import pytest
from vine import promise

from kombu.asynchronous import ERR, READ, WRITE, Hub
from kombu.asynchronous import hub as _hub
from kombu.asynchronous.debug import _rcb, callback_for, repr_flag
from kombu.asynchronous.hub import (Stop, _dummy_context, _raise_stop_error,
                                    get_event_loop, set_event_loop)
from kombu.asynchronous.semaphore import DummyLock, LaxBoundedSemaphore


class File:

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

    def fileno(self):
        return self.fd

    def __eq__(self, other):
        if isinstance(other, File):
            return self.fd == other.fd
        return NotImplemented

    def __hash__(self):
        return hash(self.fd)


def test_DummyLock():
    with DummyLock():
        pass


class test_LaxBoundedSemaphore:

    def test_acquire_release(self):
        x = LaxBoundedSemaphore(2)

        c1 = Mock()
        x.acquire(c1, 1)
        assert x.value == 1
        c1.assert_called_with(1)

        c2 = Mock()
        x.acquire(c2, 2)
        assert x.value == 0
        c2.assert_called_with(2)

        c3 = Mock()
        x.acquire(c3, 3)
        assert x.value == 0
        c3.assert_not_called()

        x.release()
        assert x.value == 0
        x.release()
        assert x.value == 1
        x.release()
        assert x.value == 2
        c3.assert_called_with(3)

    def test_repr(self):
        assert repr(LaxBoundedSemaphore(2))

    def test_bounded(self):
        x = LaxBoundedSemaphore(2)
        for i in range(100):
            x.release()
        assert x.value == 2

    def test_grow_shrink(self):
        x = LaxBoundedSemaphore(1)
        assert x.initial_value == 1
        cb1 = Mock()
        x.acquire(cb1, 1)
        cb1.assert_called_with(1)
        assert x.value == 0

        cb2 = Mock()
        x.acquire(cb2, 2)
        cb2.assert_not_called()
        assert x.value == 0

        cb3 = Mock()
        x.acquire(cb3, 3)
        cb3.assert_not_called()

        x.grow(2)
        cb2.assert_called_with(2)
        cb3.assert_called_with(3)
        assert x.value == 2
        assert x.initial_value == 3

        assert not x._waiting
        x.grow(3)
        for i in range(x.initial_value):
            assert x.acquire(Mock())
        assert not x.acquire(Mock())
        x.clear()

        x.shrink(3)
        for i in range(x.initial_value):
            assert x.acquire(Mock())
        assert not x.acquire(Mock())
        assert x.value == 0

        for i in range(100):
            x.release()
        assert x.value == x.initial_value

    def test_clear(self):
        x = LaxBoundedSemaphore(10)
        for i in range(11):
            x.acquire(Mock())
        assert x._waiting
        assert x.value == 0

        x.clear()
        assert not x._waiting
        assert x.value == x.initial_value


class test_Utils:

    def setup_method(self):
        self._prev_loop = get_event_loop()

    def teardown_method(self):
        set_event_loop(self._prev_loop)

    def test_get_set_event_loop(self):
        set_event_loop(None)
        assert _hub._current_loop is None
        assert get_event_loop() is None
        hub = Hub()
        set_event_loop(hub)
        assert _hub._current_loop is hub
        assert get_event_loop() is hub

    def test_dummy_context(self):
        with _dummy_context():
            pass

    def test_raise_stop_error(self):
        with pytest.raises(Stop):
            _raise_stop_error()


class test_Hub:

    def setup_method(self):
        self.hub = Hub()

    def teardown_method(self):
        self.hub.close()

    def test_reset(self):
        self.hub.close = Mock(name='close')
        self.hub._create_poller = Mock(name='_create_poller')
        self.hub.reset()
        self.hub.close.assert_called_with()
        self.hub._create_poller.assert_called_with()

    def test__close_poller__no_poller(self):
        self.hub.poller = None
        self.hub._close_poller()

    def test__close_poller(self):
        poller = self.hub.poller = Mock(name='poller')
        self.hub._close_poller()
        poller.close.assert_called_with()
        assert self.hub._poller is None

    def test_stop(self):
        self.hub.call_soon = Mock(name='call_soon')
        self.hub.stop()
        self.hub.call_soon.assert_called_with(_raise_stop_error)

    @patch('kombu.asynchronous.hub.promise')
    def test_call_soon(self, promise):
        callback = Mock(name='callback')
        ret = self.hub.call_soon(callback, 1, 2, 3)
        promise.assert_called_with(callback, (1, 2, 3))
        assert promise() in self.hub._ready
        assert ret is promise()

    def test_call_soon_uses_lock(self):
        callback = Mock(name='callback')
        with patch.object(self.hub, '_ready_lock', autospec=True) as lock:
            self.hub.call_soon(callback)
            lock.__enter__.assert_called_once()

    def test_call_soon__promise_argument(self):
        callback = promise(Mock(name='callback'), (1, 2, 3))
        ret = self.hub.call_soon(callback)
        assert ret is callback
        assert ret in self.hub._ready

    def test_call_later(self):
        callback = Mock(name='callback')
        self.hub.timer = Mock(name='hub.timer')
        self.hub.call_later(10.0, callback, 1, 2)
        self.hub.timer.call_after.assert_called_with(10.0, callback, (1, 2))

    def test_call_at(self):
        callback = Mock(name='callback')
        self.hub.timer = Mock(name='hub.timer')
        self.hub.call_at(21231122, callback, 1, 2)
        self.hub.timer.call_at.assert_called_with(21231122, callback, (1, 2))

    def test_repr(self):
        assert repr(self.hub)

    def test_repr_flag(self):
        assert repr_flag(READ) == 'R'
        assert repr_flag(WRITE) == 'W'
        assert repr_flag(ERR) == '!'
        assert repr_flag(READ | WRITE) == 'RW'
        assert repr_flag(READ | ERR) == 'R!'
        assert repr_flag(WRITE | ERR) == 'W!'
        assert repr_flag(READ | WRITE | ERR) == 'RW!'

    def test_repr_callback_rcb(self):

        def f():
            pass

        assert _rcb(f) == f.__name__
        assert _rcb('foo') == 'foo'

    @patch('kombu.asynchronous.hub.poll')
    def test_start_stop(self, poll):
        self.hub = Hub()
        poll.assert_called_with()

        poller = self.hub.poller
        self.hub.stop()
        mock_callback = Mock()
        self.hub._ready = {mock_callback}
        self.hub.close()
        poller.close.assert_called_with()
        mock_callback.assert_called_once_with()
        assert self.hub._ready == set()

    def test_poller_regeneration_on_access(self):
        self.hub = Hub()
        assert self.hub.poller
        self.hub.stop()
        self.hub._ready = set()
        self.hub.close()
        assert self.hub._poller is None
        assert self.hub.poller, 'It should be regenerated automatically!'

    def test_fire_timers(self):
        self.hub.timer = Mock()
        self.hub.timer._queue = []
        assert self.hub.fire_timers(
            min_delay=42.324, max_delay=32.321) == 32.321

        self.hub.timer._queue = [1]
        self.hub.scheduler = iter([(3.743, None)])
        assert self.hub.fire_timers() == 3.743

        e1, e2, e3 = Mock(), Mock(), Mock()
        entries = [e1, e2, e3]

        def reset():
            return [m.reset() for m in [e1, e2, e3]]

        def se():
            while 1:
                while entries:
                    yield None, entries.pop()
                yield 3.982, None
        self.hub.scheduler = se()

        assert self.hub.fire_timers(max_timers=10) == 3.982
        for E in [e3, e2, e1]:
            E.assert_called_with()
        reset()

        entries[:] = [Mock() for _ in range(11)]
        keep = list(entries)
        assert self.hub.fire_timers(
            max_timers=10, min_delay=1.13) == 1.13
        for E in reversed(keep[1:]):
            E.assert_called_with()
        reset()
        assert self.hub.fire_timers(max_timers=10) == 3.982
        keep[0].assert_called_with()

    def test_fire_timers_raises(self):
        eback = Mock()
        eback.side_effect = KeyError('foo')
        self.hub.timer = Mock()
        self.hub.scheduler = iter([(0, eback)])
        with pytest.raises(KeyError):
            self.hub.fire_timers(propagate=(KeyError,))

        eback.side_effect = ValueError('foo')
        self.hub.scheduler = iter([(0, eback)])
        with patch('kombu.asynchronous.hub.logger') as logger:
            with pytest.raises(StopIteration):
                self.hub.fire_timers()
            logger.error.assert_called()

        eback.side_effect = MemoryError('foo')
        self.hub.scheduler = iter([(0, eback)])
        with pytest.raises(MemoryError):
            self.hub.fire_timers()

        eback.side_effect = OSError()
        eback.side_effect.errno = errno.ENOMEM
        self.hub.scheduler = iter([(0, eback)])
        with pytest.raises(OSError):
            self.hub.fire_timers()

        eback.side_effect = OSError()
        eback.side_effect.errno = errno.ENOENT
        self.hub.scheduler = iter([(0, eback)])
        with patch('kombu.asynchronous.hub.logger') as logger:
            with pytest.raises(StopIteration):
                self.hub.fire_timers()
            logger.error.assert_called()

    def test_add_raises_ValueError(self):
        self.hub.poller = Mock(name='hub.poller')
        self.hub.poller.register.side_effect = ValueError()
        self.hub._discard = Mock(name='hub.discard')
        with pytest.raises(ValueError):
            self.hub.add(2, Mock(), READ)
        self.hub._discard.assert_called_with(2)

    def test_remove_reader(self):
        self.hub.poller = Mock(name='hub.poller')
        self.hub.add(2, Mock(), READ)
        self.hub.add(2, Mock(), WRITE)
        self.hub.remove_reader(2)
        assert 2 not in self.hub.readers
        assert 2 in self.hub.writers

    def test_remove_reader__not_writeable(self):
        self.hub.poller = Mock(name='hub.poller')
        self.hub.add(2, Mock(), READ)
        self.hub.remove_reader(2)
        assert 2 not in self.hub.readers

    def test_remove_writer(self):
        self.hub.poller = Mock(name='hub.poller')
        self.hub.add(2, Mock(), READ)
        self.hub.add(2, Mock(), WRITE)
        self.hub.remove_writer(2)
        assert 2 in self.hub.readers
        assert 2 not in self.hub.writers

    def test_remove_writer__not_readable(self):
        self.hub.poller = Mock(name='hub.poller')
        self.hub.add(2, Mock(), WRITE)
        self.hub.remove_writer(2)
        assert 2 not in self.hub.writers

    def test_add__consolidate(self):
        self.hub.poller = Mock(name='hub.poller')
        self.hub.add(2, Mock(), WRITE, consolidate=True)
        assert 2 in self.hub.consolidate
        assert self.hub.writers[2] is None

    @patch('kombu.asynchronous.hub.logger')
    def test_on_callback_error(self, logger):
        self.hub.on_callback_error(Mock(name='callback'), KeyError())
        logger.error.assert_called()

    def test_loop_property(self):
        self.hub._loop = None
        self.hub.create_loop = Mock(name='hub.create_loop')
        assert self.hub.loop is self.hub.create_loop()
        assert self.hub._loop is self.hub.create_loop()

    def test_run_forever(self):
        self.hub.run_once = Mock(name='hub.run_once')
        self.hub.run_once.side_effect = Stop()
        self.hub.run_forever()

    def test_run_once(self):
        self.hub._loop = iter([1])
        self.hub.run_once()
        self.hub.run_once()
        assert self.hub._loop is None

    def test_repr_active(self):
        self.hub.readers = {1: Mock(), 2: Mock()}
        self.hub.writers = {3: Mock(), 4: Mock()}
        for value in list(
                self.hub.readers.values()) + list(self.hub.writers.values()):
            value.__name__ = 'mock'
        assert self.hub.repr_active()

    def test_repr_events(self):
        self.hub.readers = {6: Mock(), 7: Mock(), 8: Mock()}
        self.hub.writers = {9: Mock()}
        for value in list(
                self.hub.readers.values()) + list(self.hub.writers.values()):
            value.__name__ = 'mock'
        assert self.hub.repr_events([
            (6, READ),
            (7, ERR),
            (8, READ | ERR),
            (9, WRITE),
            (10, 13213),
        ])

    def test_callback_for(self):
        reader, writer = Mock(), Mock()
        self.hub.readers = {6: reader}
        self.hub.writers = {7: writer}

        assert callback_for(self.hub, 6, READ) == reader
        assert callback_for(self.hub, 7, WRITE) == writer
        with pytest.raises(KeyError):
            callback_for(self.hub, 6, WRITE)
        assert callback_for(self.hub, 6, WRITE, 'foo') == 'foo'

    def test_add_remove_readers(self):
        P = self.hub.poller = Mock()

        read_A = Mock()
        read_B = Mock()
        self.hub.add_reader(10, read_A, 10)
        self.hub.add_reader(File(11), read_B, 11)

        P.register.assert_has_calls([
            call(10, self.hub.READ | self.hub.ERR),
            call(11, self.hub.READ | self.hub.ERR),
        ], any_order=True)

        assert self.hub.readers[10] == (read_A, (10,))
        assert self.hub.readers[11] == (read_B, (11,))

        self.hub.remove(10)
        assert 10 not in self.hub.readers
        self.hub.remove(File(11))
        assert 11 not in self.hub.readers
        P.unregister.assert_has_calls([
            call(10), call(11),
        ])

    def test_can_remove_unknown_fds(self):
        self.hub.poller = Mock()
        self.hub.remove(30)
        self.hub.remove(File(301))

    def test_remove__unregister_raises(self):
        self.hub.poller = Mock()
        self.hub.poller.unregister.side_effect = OSError()

        self.hub.remove(313)

    def test_add_writers(self):
        P = self.hub.poller = Mock()

        write_A = Mock()
        write_B = Mock()
        self.hub.add_writer(20, write_A)
        self.hub.add_writer(File(21), write_B)

        P.register.assert_has_calls([
            call(20, self.hub.WRITE),
            call(21, self.hub.WRITE),
        ], any_order=True)

        assert self.hub.writers[20], (write_A == ())
        assert self.hub.writers[21], (write_B == ())

        self.hub.remove(20)
        assert 20 not in self.hub.writers
        self.hub.remove(File(21))
        assert 21 not in self.hub.writers
        P.unregister.assert_has_calls([
            call(20), call(21),
        ])

    def test_enter__exit(self):
        P = self.hub.poller = Mock()
        on_close = Mock()
        self.hub.on_close.add(on_close)

        try:
            read_A = Mock()
            read_B = Mock()
            self.hub.add_reader(10, read_A)
            self.hub.add_reader(File(11), read_B)
            write_A = Mock()
            write_B = Mock()
            self.hub.add_writer(20, write_A)
            self.hub.add_writer(File(21), write_B)
            assert self.hub.readers
            assert self.hub.writers
        finally:
            assert self.hub.poller
            self.hub.close()
        assert not self.hub.readers
        assert not self.hub.writers

        P.unregister.assert_has_calls([
            call(10), call(11), call(20), call(21),
        ], any_order=True)

        on_close.assert_called_with(self.hub)

    def test_scheduler_property(self):
        hub = Hub(timer=[1, 2, 3])
        assert list(hub.scheduler), [1, 2 == 3]

    def test_loop__tick_callbacks(self):
        ticks = [Mock(name='cb1'), Mock(name='cb2')]
        self.hub.on_tick = list(ticks)

        next(self.hub.loop)

        ticks[0].assert_called_once_with()
        ticks[1].assert_called_once_with()

    def test_loop__tick_callbacks_on_ticks_change(self):
        def callback_1():
            ticks.remove(ticks_list[0])
            return Mock(name='cb1')

        ticks_list = [Mock(wraps=callback_1), Mock(name='cb2')]
        ticks = set(ticks_list)

        self.hub.on_tick = ticks
        self.hub.poller.unregister = Mock()

        next(self.hub.loop)
        next(self.hub.loop)

        ticks_list[0].assert_has_calls([call()])
        ticks_list[1].assert_has_calls([call(), call()])

    def test_loop__todo(self):
        deferred = Mock(name='cb_deferred')

        def defer():
            self.hub.call_soon(deferred)

        callbacks = [Mock(name='cb1', wraps=defer), Mock(name='cb2')]
        for cb in callbacks:
            self.hub.call_soon(cb)
        self.hub._ready.add(None)

        next(self.hub.loop)

        callbacks[0].assert_called_once_with()
        callbacks[1].assert_called_once_with()
        deferred.assert_not_called()

    def test_loop__no_todo_tick_delay(self):
        cb = Mock(name='parent')
        cb.todo, cb.tick, cb.poller = Mock(), Mock(), Mock()
        cb.poller.poll.side_effect = lambda obj: ()
        self.hub.poller = cb.poller
        self.hub.add(2, Mock(), READ)
        self.hub.call_soon(cb.todo)
        self.hub.on_tick = [cb.tick]

        next(self.hub.loop)

        cb.assert_has_calls([
            call.todo(),
            call.tick(),
            call.poller.poll(ANY),
        ])

    def test__pop_ready_pops_ready_items(self):
        self.hub._ready.add(None)
        ret = self.hub._pop_ready()
        assert ret == {None}
        assert self.hub._ready == set()

    def test__pop_ready_uses_lock(self):
        with patch.object(self.hub, '_ready_lock', autospec=True) as lock:
            self.hub._pop_ready()
            lock.__enter__.assert_called_once()
