"""Local deadline arithmetic fixture. Python 3.9+, stdlib only.
No clock changes, sleeps, networking, model calls or external effects.
"""
import argparse
import time
import unittest

NS = 1_000_000_000


class Deadline:
    def __init__(self, budget_ns, clock=time.monotonic_ns):
        if type(budget_ns) is not int or budget_ns <= 0:
            raise ValueError('positive integer nanoseconds required')
        self.clock = clock
        self.end = clock() + budget_ns

    def remaining_ns(self):
        return max(0, self.end - self.clock())

    def timeout_seconds(self, cap_ns):
        if type(cap_ns) is not int or cap_ns <= 0:
            raise ValueError('positive integer cap required')
        remaining = self.remaining_ns()
        if remaining == 0:
            raise TimeoutError('run deadline exhausted')
        return min(cap_ns, remaining) / NS


class Clock:
    def __init__(self):
        self.wall = 1000 * NS
        self.mono = 0

    def advance(self, seconds, wall_adjustment=0):
        self.mono += seconds * NS
        self.wall += (seconds + wall_adjustment) * NS


def demo():
    c = Clock()
    wall_end = c.wall + 30 * NS
    deadline = Deadline(30 * NS, lambda: c.mono)
    c.advance(20, wall_adjustment=-120)
    print('after 20s and wall clock -120s:')
    print('wall remaining:', (wall_end - c.wall) // NS, 'seconds')
    print('monotonic remaining:', deadline.remaining_ns() // NS, 'seconds')
    c.advance(10)
    print('at 30s:', deadline.remaining_ns() // NS, 'seconds remain')
    c = Clock()
    deadline = Deadline(30 * NS, lambda: c.mono)
    c.advance(12)
    c.advance(4)
    print('retry after 12s work + 4s backoff:', deadline.timeout_seconds(30 * NS), 'seconds')


class Tests(unittest.TestCase):
    def setUp(self):
        self.c = Clock()
        self.d = Deadline(30 * NS, lambda: self.c.mono)

    def test_backward_wall_jump(self):
        end = self.c.wall + 30 * NS
        self.c.advance(20, -120)
        self.assertEqual((end - self.c.wall) // NS, 130)
        self.assertEqual(self.d.remaining_ns(), 10 * NS)

    def test_forward_wall_jump(self):
        self.c.advance(5, 120)
        self.assertEqual(self.d.remaining_ns(), 25 * NS)

    def test_exact_expiry(self):
        self.c.advance(30)
        with self.assertRaises(TimeoutError):
            self.d.timeout_seconds(NS)

    def test_overdue_clamped(self):
        self.c.advance(31)
        self.assertEqual(self.d.remaining_ns(), 0)

    def test_attempt_cap(self):
        self.assertEqual(self.d.timeout_seconds(5 * NS), 5.0)

    def test_retry_and_backoff_share_budget(self):
        self.c.advance(12)
        self.c.advance(4)
        self.assertEqual(self.d.timeout_seconds(30 * NS), 14.0)

    def test_queue_wait_spends_budget(self):
        self.c.advance(29)
        self.assertEqual(self.d.timeout_seconds(10 * NS), 1.0)

    def test_reset_would_extend_run(self):
        self.c.advance(29)
        reset = Deadline(30 * NS, lambda: self.c.mono)
        self.assertEqual(reset.remaining_ns(), 30 * NS)
        self.assertEqual(self.d.remaining_ns(), NS)

    def test_invalid_budget(self):
        for value in (0, -1, True, 1.5, float('nan'), float('inf')):
            with self.subTest(value=value), self.assertRaises(ValueError):
                Deadline(value)

    def test_invalid_cap(self):
        for value in (0, -1, True, 1.5):
            with self.subTest(value=value), self.assertRaises(ValueError):
                self.d.timeout_seconds(value)

    def test_default_clock(self):
        d = Deadline(60 * NS)
        self.assertIs(d.clock, time.monotonic_ns)
        self.assertLessEqual(d.remaining_ns(), 60 * NS)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--test', action='store_true')
    args = parser.parse_args()
    if args.test:
        unittest.main(argv=['deadline-tests'], verbosity=2)
    else:
        demo()
