"""
Exercise 3: class-based decorators, class decorators, and memoization

STEP 1: Implement the class-based decorator `CountCalls`. `@CountCalls` means
        `f = CountCalls(f)`, so instances must be callable via __call__ and
        must expose a `.count` attribute holding the number of calls so far.
        Use functools.update_wrapper so `.__name__` still reports the
        original function's name.

STEP 2: Implement `memoize(func)` — a decorator holding a dict cache keyed by
        the positional-args tuple. On a cache hit it must NOT call `func`.
        Expose the cache as an attribute `wrapper.cache` so it is inspectable.

STEP 3: Implement the CLASS decorator `add_ordering(cls)`. It receives the
        class object itself. Give the class an `__eq__` and an `__lt__` that
        both compare `self.key()` against `other.key()`, then RETURN cls.

STEP 4: Implement `method_timer_free(func)` — a decorator applied to an
        INSTANCE METHOD. Its wrapper must forward `self` through *args like
        any other positional argument, and record the method's name into the
        list stored on the instance at `self.log`.
"""

import functools

# ---------------- STEP 1 ----------------


class CountCalls:
    def __init__(self, func):
        # TODO: call functools.update_wrapper(self, func), store func,
        # and initialise self.count = 0
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        # TODO: increment self.count, then return self.func(*args, **kwargs)
        return None


# ---------------- STEP 2 ----------------


def memoize(func):
    # TODO: create `cache = {}`; the @functools.wraps wrapper looks args up in
    # cache, computes+stores only on a miss, and returns the cached value.
    # Attach the dict as wrapper.cache before returning the wrapper.
    return func


# ---------------- STEP 3 ----------------


def add_ordering(cls):
    # TODO: define __eq__ and __lt__ comparing self.key() to other.key(),
    # attach them to cls, and return cls
    return cls


# ---------------- STEP 4 ----------------


def method_timer_free(func):
    # TODO: @functools.wraps wrapper whose FIRST positional arg is `self`.
    # Append func.__name__ to that instance's `.log` list, then delegate.
    return func


@CountCalls
def ping():
    return "pong"


calls_to_slow = {"n": 0}


@memoize
def slow_square(n):
    calls_to_slow["n"] += 1
    return n * n


@add_ordering
class Version:
    def __init__(self, major, minor):
        self.major = major
        self.minor = minor

    def key(self):
        return (self.major, self.minor)


class Service:
    def __init__(self):
        self.log = []

    @method_timer_free
    def handle(self, payload):
        return payload.upper()


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

ping()
ping()
checks.append(("CountCalls returns the value", ping(), "pong"))
checks.append(("CountCalls counts calls", ping.count, 3))
checks.append(("update_wrapper copied __name__", ping.__name__, "ping"))

calls_to_slow["n"] = 0
slow_square(4)
slow_square(4)
slow_square(5)
checks.append(("memoize returns correct value", slow_square(4), 16))
checks.append(("memoize skips repeat work", calls_to_slow["n"], 2))
checks.append(("cache is exposed and keyed by args", sorted(getattr(slow_square, "cache", {}).keys()), [(4,), (5,)]))

a, b, c = Version(1, 2), Version(1, 2), Version(1, 10)
checks.append(("class decorator added __eq__", a == b, True))
checks.append(("class decorator added __lt__", a < c, True))
checks.append(("__lt__ is not accidentally symmetric", c < a, False))

svc = Service()
checks.append(("method decorator forwards self and args", svc.handle("ok"), "OK"))
checks.append(("method decorator wrote to the instance", svc.log, ["handle"]))

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