"""Run: python3 test_claims.py   (or: python3 -m pytest test_claims.py -q — no extra deps needed either way)"""
from contextlib import contextmanager
from datetime import datetime, timedelta
from claims import Task, Node, Store, Conflict, Forbidden, Stale, claim, save_draft, worklist


@contextmanager
def raises(exc):
    """Assert that the block raises `exc` (tiny stand-in for pytest's raises)."""
    try:
        yield
    except exc:
        return
    raise AssertionError(f"expected {exc.__name__} to be raised")

T0 = datetime(2026, 9, 1, 9, 0)
H = timedelta(hours=1)


def make_store():
    tasks = {"t1": Task("t1", "gotham"), "t2": Task("t2", "gotham", status="paused")}
    nodes = {
        "t1-author": Node("t1-author", "t1", "author", 0),
        "t1-review": Node("t1-review", "t1", "review", 1),
        "t1-qa":     Node("t1-qa",     "t1", "qa",     2),
        "t2-author": Node("t2-author", "t2", "author", 0),
    }
    seats = {
        ("alice", "gotham"): {"author", "review", "qa"},
        ("bob",   "gotham"): {"author", "review", "qa"},
        ("carol", "gotham"): {"review", "qa"},
        ("dave",  "gotham"): {"author"},
    }
    return Store(tasks, nodes, seats, idle_hours={"gotham": 8})


def test_claim_parked_node_and_resume():
    s = make_store()
    n = claim("t1-author", "alice", s, T0)
    assert (n.status, n.assignee, n.claimed_at) == ("running", "alice", T0)
    assert claim("t1-author", "alice", s, T0 + H) is n            # resume, no error


def test_claim_held_by_someone_else_conflicts():
    s = make_store(); claim("t1-author", "alice", s, T0)
    with raises(Conflict):
        claim("t1-author", "bob", s, T0 + H)


def test_seat_required():
    s = make_store()
    with raises(Forbidden):
        claim("t1-review", "dave", s, T0)                          # dave only holds 'author'


def test_four_eyes_across_all_earlier_steps():
    s = make_store()
    claim("t1-author", "alice", s, T0); s.nodes["t1-author"].status = "done"
    claim("t1-review", "bob", s, T0);   s.nodes["t1-review"].status = "done"
    with raises(Forbidden):
        claim("t1-qa", "bob", s, T0)                               # reviewer cannot also QA
    with raises(Forbidden):
        claim("t1-qa", "alice", s, T0)                             # author cannot QA either
    assert claim("t1-qa", "carol", s, T0).assignee == "carol"


def test_idle_node_can_be_reclaimed_after_more_than_a_day():
    s = make_store(); claim("t1-author", "alice", s, T0)
    later = T0 + 30 * H                                            # idle_hours is 8
    assert claim("t1-author", "bob", s, later).assignee == "bob"
    assert "t1-author" not in worklist("alice", s, later)["waiting_on_you"]


def test_stale_draft_is_rejected():
    s = make_store(); claim("t1-author", "alice", s, T0)
    assert save_draft("t1-author", "alice", "v1", expected_revision=0, store=s) == 1
    assert save_draft("t1-author", "alice", "v2", expected_revision=1, store=s) == 2
    with raises(Stale):
        save_draft("t1-author", "alice", "old tab", expected_revision=1, store=s)
    assert s.nodes["t1-author"].draft == "v2"


def test_only_assignee_may_save():
    s = make_store(); claim("t1-author", "alice", s, T0)
    with raises(Forbidden):
        save_draft("t1-author", "bob", "x", expected_revision=0, store=s)


def test_worklist_excludes_paused_tasks_and_is_disjoint():
    s = make_store(); claim("t1-author", "alice", s, T0)
    wl = worklist("alice", s, T0 + H)
    assert wl["waiting_on_you"] == ["t1-author"]
    assert "t2-author" not in wl["available"]                      # task t2 is paused
    assert not set(wl["waiting_on_you"]) & set(wl["available"])


if __name__ == "__main__":
    import sys, traceback
    fails = 0
    for name, fn in [(n, f) for n, f in list(globals().items()) if n.startswith("test_")]:
        try:
            fn(); print(f"PASS {name}")
        except BaseException:
            fails += 1; print(f"FAIL {name}"); traceback.print_exc()
    sys.exit(1 if fails else 0)
