"""Stdlib-only tests (no pytest): plain asserts in functions called from a
__main__ block. Run: `python3 test_rate_limiter.py`.

The headline test locks in the verified demo trace — the exact point where
the two algorithms diverge (see commentary.md, section 6). The rest are small
unit checks on each limiter in isolation.
"""

from rate_limiter import TokenBucket, SlidingWindowLog


def test_demo_trace_divergence():
    """Same trace, same average rate (5 req / 5s), two divergences."""
    bucket = TokenBucket(capacity=5, refill_rate=1.0)
    window = SlidingWindowLog(max_requests=5, window=5.0)

    # t=0.0: initial burst of 5 — both allow every one.
    for _ in range(5):
        assert bucket.allow(0.0) is True
        assert window.allow(0.0) is True

    # t=2.5: lone request — bucket has refilled ~2.5 tokens and allows;
    # the original 5 are still inside the 5s window, so the log denies.
    assert bucket.allow(2.5) is True
    assert window.allow(2.5) is False

    # t=5.0: second burst of 5. The original burst ages out exactly at the
    # boundary, so the window resets and allows all 5. The bucket has only
    # regenerated ~4 tokens, so it allows 4 then denies the 5th.
    bucket_results = [bucket.allow(5.0) for _ in range(5)]
    window_results = [window.allow(5.0) for _ in range(5)]
    assert bucket_results == [True, True, True, True, False]
    assert window_results == [True, True, True, True, True]


def test_bucket_never_exceeds_capacity_after_long_idle():
    """Tokens are capped at capacity no matter how long the bucket sits idle."""
    bucket = TokenBucket(capacity=5, refill_rate=1.0)
    # Idle for 1000s would refill 1000 tokens if uncapped; only 5 may exist,
    # so exactly 5 requests succeed in a burst before the 6th is denied.
    allowed = [bucket.allow(1000.0) for _ in range(6)]
    assert allowed == [True, True, True, True, True, False]


def test_bucket_steady_state_rate():
    """Once drained, the bucket admits one request per 1/refill_rate seconds."""
    bucket = TokenBucket(capacity=1, refill_rate=1.0)
    assert bucket.allow(0.0) is True   # spends the initial token
    assert bucket.allow(0.5) is False  # only 0.5 tokens refilled
    assert bucket.allow(1.0) is True   # a full token is back


def test_window_evicts_entries_older_than_window():
    """Timestamps strictly older than `window` seconds fall out of the log."""
    window = SlidingWindowLog(max_requests=2, window=5.0)
    assert window.allow(0.0) is True   # log: [0.0]
    assert window.allow(2.0) is True   # log: [0.0, 2.0] — now full
    assert window.allow(3.0) is False  # both still in window -> denied
    # At t=6.0 the entry at 0.0 is older than `window` (cutoff = 6.0 - 5.0 =
    # 1.0, and 0.0 <= 1.0) so it is evicted; 2.0 survives, freeing one slot.
    assert window.allow(6.0) is True   # log: [2.0, 6.0]
    assert window.allow(6.0) is False  # full again


TESTS = [
    test_demo_trace_divergence,
    test_bucket_never_exceeds_capacity_after_long_idle,
    test_bucket_steady_state_rate,
    test_window_evicts_entries_older_than_window,
]


if __name__ == "__main__":
    failed = 0
    for test in TESTS:
        try:
            test()
        except AssertionError as exc:
            failed += 1
            print(f"FAIL  {test.__name__}: {exc}")
        else:
            print(f"PASS  {test.__name__}")
    if failed:
        print(f"\n{failed} of {len(TESTS)} tests FAILED")
        raise SystemExit(1)
    print(f"\nAll {len(TESTS)} tests PASSED")
