"""
Exercise 1: speed up I/O-bound work with threads (easy)

fetch(url) simulates a network call that takes 0.2 s (it sleeps, which releases the GIL).

Implement fetch_all(urls, max_workers) so that it returns the results IN THE SAME ORDER as urls,
using a ThreadPoolExecutor with max_workers threads. Fetching 10 urls with 10 workers must take
well under the sequential 2 seconds.

Also implement SafeCounter.increment() so that many threads incrementing concurrently never lose updates
(use a threading.Lock).
"""

import threading
import time
from concurrent.futures import ThreadPoolExecutor


def fetch(url):
    time.sleep(0.2)
    return url.upper()


def fetch_all(urls, max_workers):
    return [fetch(u) for u in urls]  # TODO: run concurrently, keep order


class SafeCounter:
    def __init__(self):
        self.value = 0

    def increment(self):
        v = self.value          # TODO: protect this read-modify-write with a lock
        time.sleep(0)
        self.value = v + 1


# --- self-check: do not modify below this line ---
checks = []
urls = [f"u{i}" for i in range(10)]
start = time.perf_counter()
out = fetch_all(urls, 10)
elapsed = time.perf_counter() - start
checks.append(("results in order", out, [u.upper() for u in urls]))
checks.append(("concurrent (< 0.8 s)", elapsed < 0.8, True))

c = SafeCounter()
def work():
    for _ in range(200):
        c.increment()
threads = [threading.Thread(target=work) for _ in range(8)]
for t in threads:
    t.start()
for t in threads:
    t.join()
checks.append(("no lost updates", c.value, 1600))

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")
