"""
Exercise 2: re-raising, exception chaining, and finally semantics (harder)

STEP 1: Implement `log_and_reraise(func, *args)` — call func(*args) inside
        a try. On ANY exception, append str(exception) to the module-level
        list LOG, then re-raise the SAME exception using a bare `raise`
        (not `raise e`, which would reset the traceback).

STEP 2: Implement `wrap_error(func, *args)` — call func(*args) inside a
        try. On ValueError, raise a RuntimeError("operation failed") using
        explicit chaining (`from`) so the ORIGINAL ValueError becomes the
        new exception's __cause__.

STEP 3: Implement `first_successful(funcs)` — funcs is a list of
        zero-argument callables. Try them IN ORDER; return the result of
        the first one that does not raise. If a callable raises, move on
        to the next. If ALL of them raise, raise a RuntimeError("all
        attempts failed") — this exception's __cause__ does not matter
        for the check below, any raise is fine.

STEP 4: Implement `risky_finally(should_raise)` — demonstrates (but must
        NOT itself swallow) a finally block: increment the module-level
        counter FINALLY_RUNS by 1 inside finally, then if should_raise is
        True, let the ValueError raised in the try propagate normally
        (do NOT use return/break/continue inside finally — that would
        swallow it, which is exactly the pitfall this exercise checks
        you avoid).
"""

LOG = []
FINALLY_RUNS = 0


def log_and_reraise(func, *args):
    # TODO: try func(*args); except Exception as e: LOG.append(str(e)); raise
    return func(*args)


def wrap_error(func, *args):
    # TODO: try func(*args); except ValueError as e: raise RuntimeError("operation failed") from e
    return func(*args)


def first_successful(funcs):
    # TODO: iterate funcs, try each, return first success; if every one
    # raises, raise RuntimeError("all attempts failed")
    pass


def risky_finally(should_raise):
    global FINALLY_RUNS
    try:
        if should_raise:
            raise ValueError("boom")
        return "ok"
    finally:
        # TODO: FINALLY_RUNS += 1 -- do NOT return/break/continue here
        pass


# --- self-check: do not modify below this line ---
checks = []


def boom(msg):
    raise ValueError(msg)


LOG.clear()
try:
    log_and_reraise(boom, "kaboom")
    raised = None
except ValueError as e:
    raised = e
checks.append(("log_and_reraise re-raises the exception", isinstance(raised, ValueError), True))
checks.append(("log_and_reraise logs the message first", LOG, ["kaboom"]))

try:
    wrap_error(boom, "bad value")
    wrapped = None
except RuntimeError as e:
    wrapped = e
checks.append(("wrap_error raises RuntimeError", isinstance(wrapped, RuntimeError), True))
checks.append(("wrap_error chains the original as __cause__", isinstance(wrapped.__cause__, ValueError) if wrapped else False, True))
checks.append(("wrap_error preserves original message in cause", str(wrapped.__cause__) if wrapped else None, "bad value"))

checks.append(("first_successful skips failing callables", first_successful([lambda: boom("x"), lambda: 42]), 42))
checks.append(("first_successful returns first success immediately", first_successful([lambda: 1, lambda: 2]), 1))

try:
    first_successful([lambda: boom("a"), lambda: boom("b")])
    all_failed = False
except RuntimeError:
    all_failed = True
checks.append(("first_successful raises when everything fails", all_failed, True))

checks.append(("risky_finally returns normally when no error", risky_finally(False), "ok"))
FINALLY_RUNS = 0
try:
    risky_finally(True)
    propagated = False
except ValueError:
    propagated = True
checks.append(("risky_finally does NOT swallow the exception", propagated, True))
checks.append(("risky_finally still ran its cleanup", FINALLY_RUNS, 1))

passed = 0
for label, actual, expected in checks:
    ok = actual == expected
    passed += ok
    print(f"{'PASS' if ok else 'FAIL'}: {label} (got {actual!r}, expected {expected!r})")

print("PASS" if passed == len(checks) else "FAIL")
