from __future__ import annotations

import unittest

from datahub.governance.circuit import CircuitBreaker
from datahub.governance.ratelimit import TokenBucket
from datahub.governance.retry import RetryError, retry_call


class FakeClock:
    def __init__(self) -> None:
        self.value = 0.0

    def __call__(self) -> float:
        return self.value


class GovernanceTests(unittest.TestCase):
    def test_token_bucket_caps_burst_at_capacity(self) -> None:
        clock = FakeClock()
        bucket = TokenBucket(rate_per_minute=300, capacity=300, clock=clock)
        ok = 0
        for _ in range(400):
            if bucket.acquire(block=False):
                ok += 1
        self.assertEqual(ok, 300)
        clock.value = 60
        self.assertTrue(bucket.acquire(block=False))

    def test_circuit_opens_after_five_failures_and_half_opens(self) -> None:
        clock = FakeClock()
        breaker = CircuitBreaker(clock=clock, open_seconds=120)
        for _ in range(5):
            breaker.record_failure("boom")
        self.assertEqual(breaker.snapshot().state, "open")
        self.assertFalse(breaker.allow())
        clock.value = 120
        self.assertEqual(breaker.snapshot().state, "half_open")
        self.assertTrue(breaker.allow())
        breaker.record_success()
        self.assertEqual(breaker.snapshot().state, "closed")

    def test_retry_exhausts(self) -> None:
        calls = {"n": 0}

        def fail():
            calls["n"] += 1
            raise RuntimeError("no")

        with self.assertRaises(RetryError):
            retry_call(fail, attempts=3, sleeper=lambda _d: None)
        self.assertEqual(calls["n"], 3)


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