"""Tests for ControlRedis wait_messages and non-blocking reconnect."""

from __future__ import annotations

import sys
import time
import unittest
from unittest.mock import MagicMock, patch

sys.modules.setdefault("redis", MagicMock())

from control.redis_io import CHAN_COMMAND, ControlRedis


class FakePubSub:
    def __init__(self, script: list[object]) -> None:
        self._script = list(script)
        self._i = 0

    def get_message(self, timeout: float = 0.0) -> dict[str, object] | None:
        if self._i >= len(self._script):
            return None
        item = self._script[self._i]
        self._i += 1
        if isinstance(item, float):
            time.sleep(item)
            return None
        return item


class TestWaitMessages(unittest.TestCase):
    @patch.object(ControlRedis, "subscribe_commands_and_temps")
    def test_returns_promptly_on_first_message(self, sub_mock: MagicMock) -> None:
        msg = {"type": "message", "channel": CHAN_COMMAND, "data": "{}"}
        sub_mock.return_value = FakePubSub([msg])
        cr = ControlRedis("redis://127.0.0.1:6379/0")
        t0 = time.monotonic()
        out = cr.wait_messages(timeout_s=5.0)
        elapsed = time.monotonic() - t0
        self.assertEqual(out, [(CHAN_COMMAND, "{}")])
        self.assertLess(elapsed, 0.5)

    @patch.object(ControlRedis, "subscribe_commands_and_temps")
    def test_timeout_returns_empty(self, sub_mock: MagicMock) -> None:
        sub_mock.return_value = FakePubSub([0.15])
        cr = ControlRedis("redis://127.0.0.1:6379/0")
        t0 = time.monotonic()
        out = cr.wait_messages(timeout_s=0.1)
        elapsed = time.monotonic() - t0
        self.assertEqual(out, [])
        self.assertGreaterEqual(elapsed, 0.08)
        self.assertLess(elapsed, 0.35)

    @patch.object(ControlRedis, "subscribe_commands_and_temps")
    def test_drains_queued_messages(self, sub_mock: MagicMock) -> None:
        m1 = {"type": "message", "channel": CHAN_COMMAND, "data": "a"}
        m2 = {"type": "message", "channel": CHAN_COMMAND, "data": "b"}
        sub_mock.return_value = FakePubSub([m1, m2, None])
        cr = ControlRedis("redis://127.0.0.1:6379/0")
        out = cr.wait_messages(timeout_s=1.0)
        self.assertEqual(out, [(CHAN_COMMAND, "a"), (CHAN_COMMAND, "b")])


class TestNonBlockingReconnect(unittest.TestCase):
    def test_ensure_returns_immediately_before_retry_after(self) -> None:
        cr = ControlRedis("redis://127.0.0.1:6379/0")
        cr._retry_after_mono = time.monotonic() + 60.0
        t0 = time.monotonic()
        client = cr._ensure()
        elapsed = time.monotonic() - t0
        self.assertIsNone(client)
        self.assertLess(elapsed, 0.05)

    @patch.object(ControlRedis, "_connect")
    def test_ensure_backoff_without_sleep(self, connect_mock: MagicMock) -> None:
        client = MagicMock()
        client.ping.side_effect = OSError("down")
        connect_mock.return_value = client
        cr = ControlRedis("redis://127.0.0.1:6379/0")
        t0 = time.monotonic()
        self.assertIsNone(cr._ensure())
        elapsed = time.monotonic() - t0
        self.assertLess(elapsed, 0.05)
        self.assertGreater(cr._retry_after_mono, time.monotonic())


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