"""Tests for MikrotikApiClient error wrapping."""
import asyncio
import unittest

from preseem.mikrotik import (MikrotikApiClient, MikrotikApiError,
                              MikrotikApiTransportError)


class TestMikrotikApiTransportWrapping(unittest.TestCase):
    """PPA-2056: verify MikrotikApiClient classifies network-layer failures as
    MikrotikApiTransportError, so callers can distinguish device-unreachable
    events from protocol/auth errors."""

    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)

    def tearDown(self):
        self.loop.close()

    def _make_client(self):
        c = MikrotikApiClient('10.1.1.1', 'user', 'pw', timeout=0.5)

        async def noop_close():
            pass

        c.close = noop_close
        return c

    def test_oserror_wraps_to_transport_error(self):
        """OSError raised during a mikrotik API call surfaces as
        MikrotikApiTransportError via the retry-loop-tail classifier,
        with the OSError chained via __cause__."""
        c = self._make_client()

        async def raise_no_route(*args, **kwargs):
            raise OSError(113, 'Connect call failed')

        c._get_results_data = raise_no_route
        with self.assertRaises(MikrotikApiTransportError) as cm:
            self.loop.run_until_complete(c._get_results('/ip/address/print', num_retries=0))
        self.assertIsInstance(cm.exception.__cause__, OSError)
        self.assertEqual(cm.exception.__cause__.errno, 113)

    def test_wait_for_timeout_wraps_to_transport_error(self):
        """asyncio.TimeoutError from asyncio.wait_for surfaces as
        MikrotikApiTransportError, chained via __cause__."""
        c = self._make_client()

        async def hang(*args, **kwargs):
            await asyncio.sleep(10)

        c._get_results_data = hang
        with self.assertRaises(MikrotikApiTransportError) as cm:
            self.loop.run_until_complete(c._get_results('/ip/address/print', num_retries=0))
        self.assertIsInstance(cm.exception.__cause__, asyncio.TimeoutError)

    def test_read_word_length_byte_eof_wraps_to_transport_error(self):
        """read_word raises MikrotikApiTransportError when the peer closes
        the connection on the first (length) byte."""
        c = self._make_client()

        class EmptyReader:

            async def read(self, n):
                return b''

        c._reader = EmptyReader()
        with self.assertRaises(MikrotikApiTransportError) as cm:
            self.loop.run_until_complete(c.read_word())
        self.assertIn('closed by remote end', str(cm.exception))
        self.assertIn('10.1.1.1', str(cm.exception))

    def test_read_word_payload_eof_wraps_to_transport_error(self):
        """read_word raises MikrotikApiTransportError when the peer closes
        mid-payload (length byte read succeeded, payload read returns
        empty).  Also verifies close() is called, matching the length-byte
        EOF path; without it, _mikrotik_probe would attempt the next
        credential on a stale writer."""
        c = self._make_client()

        class MidPayloadDropReader:
            """First read returns a length byte announcing 5 bytes of payload;
            second read (for the payload) returns empty (EOF)."""

            def __init__(self):
                self.calls = 0

            async def read(self, n):
                self.calls += 1
                if self.calls == 1:
                    return bytes([0x05])
                return b''

        c._reader = MidPayloadDropReader()
        close_calls = {'n': 0}

        async def counting_close():
            close_calls['n'] += 1

        c.close = counting_close
        with self.assertRaises(MikrotikApiTransportError) as cm:
            self.loop.run_until_complete(c.read_word())
        self.assertIn('closed by remote end', str(cm.exception))
        self.assertIn('10.1.1.1', str(cm.exception))
        self.assertEqual(close_calls['n'], 1,
                         'read_word payload-EOF must close() before raising')

    def test_protocol_error_is_not_wrapped(self):
        """MikrotikApiError (protocol/auth) propagates unchanged so callers
        can keep it visible in Sentry."""
        c = self._make_client()

        async def raise_auth(*args, **kwargs):
            raise MikrotikApiError('not enough permissions')

        c._get_results_data = raise_auth
        with self.assertRaises(MikrotikApiError) as cm:
            self.loop.run_until_complete(c._get_results('/ip/address/print', num_retries=0))
        self.assertNotIsInstance(cm.exception, MikrotikApiTransportError)


if __name__ == '__main__':
    unittest.main()
