from __future__ import annotations

from dataclasses import dataclass, field


@dataclass
class Run:
    run_id: str
    status: str = "QUEUED"
    version: int = 0
    events: list = field(default_factory=list)

    def change(self, new_status):
        allowed = {
            "QUEUED": {"RUNNING", "CANCELLED"},
            "RUNNING": {"WAITING_APPROVAL", "FAILED", "SUCCEEDED", "CANCELLED"},
            "WAITING_APPROVAL": {"RUNNING", "CANCELLED"},
            "FAILED": set(), "SUCCEEDED": set(), "CANCELLED": set(),
        }
        if new_status not in allowed[self.status]:
            raise ValueError("illegal transition")
        self.status = new_status
        self.version += 1
        self.events.append((self.version, "run.status_changed", new_status))

    def after(self, cursor):
        return [event for event in self.events if event[0] > cursor]


class App:
    def __init__(self):
        self.requests = {}
        self.runs = {}

    def create(self, principal, key, digest):
        identity = (principal, key)
        if identity in self.requests:
            old_digest, run_id = self.requests[identity]
            if old_digest != digest:
                raise ValueError("idempotency key reused with different request")
            return self.runs[run_id]
        run_id = f"run-{len(self.runs) + 1}"
        run = Run(run_id)
        self.runs[run_id] = run
        self.requests[identity] = (digest, run_id)
        return run


def decide_approval(run, expected_version, approve):
    if run.version != expected_version:
        raise ValueError("approval version conflict")
    if run.status != "WAITING_APPROVAL":
        raise ValueError("approval not pending")
    run.change("RUNNING" if approve else "CANCELLED")


def test_contract():
    app = App()
    run = app.create("user-a", "key-1", "digest-1")
    assert app.create("user-a", "key-1", "digest-1") is run
    assert len(app.runs) == 1
    try:
        app.create("user-a", "key-1", "different")
        assert False
    except ValueError:
        pass
    run.change("RUNNING")
    run.change("WAITING_APPROVAL")
    assert len(run.after(1)) == 1
    try:
        decide_approval(run, expected_version=1, approve=True)
        assert False
    except ValueError:
        pass
    decide_approval(run, expected_version=2, approve=True)
    run.change("SUCCEEDED")
    assert run.status == "SUCCEEDED"
    assert len(run.after(0)) == 4
    print("PASS: idempotency, conflict, cursor, approval, terminal state")


if __name__ == "__main__":
    test_contract()
