"""Zero-failure, fixed-sample binomial bound. Standard library only.

This calculator cannot verify independence, sampling quality or grader accuracy.
It deliberately refuses nonzero failures and incomplete outcome accounting.
Run with Python 3.9+; no files, network requests or model calls are made.
"""
import math
import unittest


def probability(value, name):
    if isinstance(value, bool) or not isinstance(value, (float, int)):
        raise ValueError(name + " must be a number")
    if not math.isfinite(value) or not 0 < value < 1:
        raise ValueError(name + " must be strictly between zero and one")
    return value


def upper_bound(n, *, failures=0, unknown=0, confidence=0.95):
    """Exact one-sided upper bound, conditional on iid Bernoulli trials."""
    if type(n) is not int or n < 1:
        raise ValueError("n must be a positive integer")
    for name, value in (("failures", failures), ("unknown", unknown)):
        if type(value) is not int or value != 0:
            raise ValueError(name + " must be exactly zero for this calculator")
    probability(confidence, "confidence")
    return -math.expm1(math.log1p(-confidence) / n)


def required_trials(target, confidence=0.95):
    """Minimum planned n giving an upper bound <= target if all trials pass."""
    probability(target, "target")
    probability(confidence, "confidence")
    n = max(1, math.ceil(math.log1p(-confidence) / math.log1p(-target)))
    # Correct possible float rounding at an integer boundary.
    while upper_bound(n, confidence=confidence) > target:
        n += 1
    while n > 1 and upper_bound(n - 1, confidence=confidence) <= target:
        n -= 1
    return n


class Tests(unittest.TestCase):
    def test_one_trial(self):
        self.assertAlmostEqual(upper_bound(1), 0.95)

    def test_inverts_probability(self):
        for n in (10, 100, 299, 1000):
            self.assertAlmostEqual((1 - upper_bound(n)) ** n, 0.05)

    def test_planning_minimality(self):
        for target in (0.1, 0.01, 0.001):
            n = required_trials(target)
            self.assertLessEqual(upper_bound(n), target)
            self.assertGreater(upper_bound(n - 1), target)

    def test_sample_size_direction(self):
        self.assertGreater(upper_bound(100), upper_bound(1000))

    def test_confidence_direction(self):
        self.assertGreater(upper_bound(100, confidence=0.99), upper_bound(100))

    def test_rejects_bad_counts(self):
        for value in (0, -1, True, 1.5, "100"):
            with self.assertRaises(ValueError):
                upper_bound(value)

    def test_rejects_failures_and_unknowns(self):
        for field in ("failures", "unknown"):
            for value in (1, -1, False, 0.0):
                with self.assertRaises(ValueError):
                    upper_bound(100, **{field: value})

    def test_rejects_bad_probabilities(self):
        for value in (0, 1, -1, float("nan"), float("inf"), True, "0.95"):
            with self.assertRaises(ValueError):
                upper_bound(100, confidence=value)
            with self.assertRaises(ValueError):
                required_trials(value)

    def test_large_sample_stability(self):
        bound = upper_bound(10**12)
        self.assertGreater(bound, 0)
        self.assertAlmostEqual(bound * 10**12, -math.log(0.05), places=9)

    def test_nondefault_confidence(self):
        self.assertAlmostEqual(upper_bound(1, confidence=0.90), 0.90)
        self.assertEqual(required_trials(0.1, confidence=0.90), 22)


if __name__ == "__main__":
    for n in (30, 100, 300, 1000):
        print(f"0/{n} failures: 95% one-sided upper bound = {upper_bound(n):.4%}", flush=True)
    for target in (0.01, 0.001):
        print(f"Target <= {target:.1%}: plan {required_trials(target)} trials, all must pass", flush=True)
    unittest.main(verbosity=2)
