import unittest
from retry_demo import retry_read, RetryPolicy, TemporaryFailure, RetryExhausted


class FakeTime:
    def __init__(self):
        self.now = 0.0
        self.waits = []

    def clock(self):
        return self.now

    def sleep(self, seconds):
        self.waits.append(seconds)
        self.now += seconds


class RetryTests(unittest.TestCase):
    def test_success_after_two_failures(self):
        t = FakeTime()
        calls = []
        def op():
            calls.append(1)
            if len(calls) < 3:
                raise TemporaryFailure("try later")
            return "ok"
        self.assertEqual(retry_read(op, RetryPolicy(), clock=t.clock, sleep=t.sleep), "ok")
        self.assertEqual(len(calls), 3)
        self.assertEqual(t.waits, [0.5, 1.0])

    def test_persistent_failure_stops_without_final_sleep(self):
        t = FakeTime()
        calls = []
        def op():
            calls.append(1)
            raise TemporaryFailure("still unavailable")
        with self.assertRaises(RetryExhausted):
            retry_read(op, RetryPolicy(), clock=t.clock, sleep=t.sleep)
        self.assertEqual(len(calls), 3)
        self.assertEqual(t.waits, [0.5, 1.0])

    def test_permanent_failure_is_not_retried(self):
        t = FakeTime()
        def op():
            raise ValueError("invalid input")
        with self.assertRaises(ValueError):
            retry_read(op, RetryPolicy(), clock=t.clock, sleep=t.sleep)
        self.assertEqual(t.waits, [])

    def test_budget_stops_before_unaffordable_wait(self):
        t = FakeTime()
        calls = []
        def op():
            calls.append(1)
            raise TemporaryFailure("not ready")
        with self.assertRaises(RetryExhausted):
            retry_read(op, RetryPolicy(max_elapsed=0.5), clock=t.clock, sleep=t.sleep)
        self.assertEqual(len(calls), 1)
        self.assertEqual(t.waits, [])

    def test_one_attempt_has_no_retry(self):
        t = FakeTime()
        with self.assertRaises(RetryExhausted):
            retry_read(lambda: (_ for _ in ()).throw(TemporaryFailure()),
                       RetryPolicy(max_attempts=1), clock=t.clock, sleep=t.sleep)
        self.assertEqual(t.waits, [])

    def test_delay_cap_applies(self):
        t = FakeTime()
        def op():
            raise TemporaryFailure()
        with self.assertRaises(RetryExhausted):
            retry_read(op, RetryPolicy(max_attempts=5, max_elapsed=20, delay_cap=1),
                       clock=t.clock, sleep=t.sleep)
        self.assertEqual(t.waits, [0.5, 1, 1, 1])

    def test_long_operation_is_not_interrupted(self):
        t = FakeTime()
        def op():
            t.now += 30
            return "late success"
        self.assertEqual(retry_read(op, RetryPolicy(max_elapsed=1),
                                   clock=t.clock, sleep=t.sleep), "late success")
        self.assertEqual(t.now, 30)


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