"""Offline retry-count fixture. Python 3.9+; no network or sleeping."""
import json
import sys
import unittest


class Transient(Exception):
    pass


class Permanent(Exception):
    pass


def retry(call, attempts):
    if type(attempts) is not int or attempts < 1:
        raise ValueError('attempts must be a positive integer, including the first call')
    for attempt in range(attempts):
        try:
            return call()
        except Transient:
            if attempt == attempts - 1:
                raise


def simulate(limits, failures=None, permanent=False):
    """Each limit includes the initial attempt. None means always fail."""
    if not limits or any(type(n) is not int or n < 1 for n in limits):
        raise ValueError('provide positive integer layer limits')
    if failures is not None and (type(failures) is not int or failures < 0):
        raise ValueError('failures must be a nonnegative integer or None')
    calls = 0

    def dependency():
        nonlocal calls
        calls += 1
        if permanent:
            raise Permanent()
        if failures is None or calls <= failures:
            raise Transient()
        return 'ok'

    call = dependency
    for limit in reversed(limits):
        inner = call
        call = lambda inner=inner, limit=limit: retry(inner, limit)
    try:
        call()
        outcome = 'success'
    except Transient:
        outcome = 'exhausted'
    except Permanent:
        outcome = 'permanent_error'
    return {'limits': list(limits), 'dependency_calls': calls, 'outcome': outcome}


class Tests(unittest.TestCase):
    def test_nested(self):
        self.assertEqual(simulate([3, 3, 3])['dependency_calls'], 27)
    def test_single_owner(self):
        self.assertEqual(simulate([1, 1, 3])['dependency_calls'], 3)
    def test_no_retries(self):
        self.assertEqual(simulate([1, 1, 1])['dependency_calls'], 1)
    def test_initial_success(self):
        self.assertEqual(simulate([3, 3, 3], 0)['dependency_calls'], 1)
    def test_transient_recovery(self):
        self.assertEqual(simulate([1, 1, 3], 2)['outcome'], 'success')
    def test_recovery_beyond_budget(self):
        self.assertEqual(simulate([1, 1, 3], 3)['outcome'], 'exhausted')
    def test_permanent(self):
        self.assertEqual(simulate([3, 3, 3], permanent=True)['dependency_calls'], 1)
    def test_invalid_limits(self):
        for limits in ([], [0], [-1], [True], [1.5]):
            with self.subTest(limits=limits), self.assertRaises(ValueError):
                simulate(limits)
    def test_invalid_failures(self):
        for failures in (-1, True, 1.5):
            with self.subTest(failures=failures), self.assertRaises(ValueError):
                simulate([1], failures)
    def test_retry_validation(self):
        with self.assertRaises(ValueError):
            retry(lambda: None, 0)


if __name__ == '__main__':
    if sys.argv[1:] == ['--test']:
        unittest.main(argv=[sys.argv[0]])
    elif sys.argv[1:]:
        raise SystemExit('usage: python3 agent-retry-amplification.py [--test]')
    else:
        print(json.dumps({'nested': simulate([3, 3, 3]),
                          'single_owner': simulate([1, 1, 3]),
                          'transient_recovery': simulate([1, 1, 3], 2)}, indent=2))
