import json, subprocess, sys
FILES = ['recovery-bindings','recovery-conflicts','recovery-round10','recovery-round11','recovery-round12','recovery-round13','recovery-round14']
def load(f):
    return json.loads(json.loads(open('/tmp/'+f+'.out').read().strip()))
a,b,c,d,r12,r13,r14 = (load(fn) for fn in FILES)
fails=[]
def chk(n,cond,val=None):
    if not cond: fails.append([n,val])
# --- round 9: an operation is bound to its own target, content and session
chk('held end does not touch a new plan', a['r1b']['staleOp'] and a['r1b']['blockedOp'] is None and a['r1b']['newPlanStatus']=='active', a['r1b'])
chk('exercise retry saves its own draft', a['r2']['saved']==['Walk 30'], a['r2'])
chk('cancelling the session retires it', a['r2cancel']['opAfterCancel'] is None)
chk('plan-create retry keeps its own draft', a['r3']['days']==7, a['r3'])
chk('deleting the data retires it', a['r4']['after'] is None)
for m,e in [('offline','offline'),('saveFails','unknown'),('conflict','conflict')]:
    f=a['fridge'][m]
    chk('fridge shows its own %s explanation' % m, f['fridgeMode'][e] and not f['planHere'] and not f['exHere'], f)
    chk('fridge %s survives going back online' % m, f['afterNormal'][e], f)
chk('a plan failure stays off the exercise form', a['crossSurface']['planHere'] and not a['crossSurface']['exHere'], a['crossSurface'])
chk('the conflict differs from what I asked for', 'skipped' in a['conflict']['theirs'] and a['conflict']['afterRead'][0]=='skipped', a['conflict'])
chk('keeping the latest retires mine', a['conflictTakeLatest']['op'] is None and a['conflictTakeLatest']['day0']=='skipped')
chk('applying mine on top runs once', a['conflictApply']['day0']=='done' and a['conflictApply']['op'] is None)
chk('an unrelated write leaves it pending', a['consume']['afterOther'] and a['consume']['day1']=='skipped', a['consume'])
chk('the same write retires it', a['consume']['opAfter'] is None and a['consume']['day0']=='done', a['consume'])
chk('a landed end retires a held end', a['consumeEnd']['opAfter'] is None and a['consumeEnd']['status']=='completed', a['consumeEnd'])
# --- round 9b
chk('creating over an existing plan offers no re-apply', b['planCreateConflict']['settled'] and not b['planCreateConflict']['reapplyable'])
chk('exercise conflict keeps both sessions', b['exConflictApply']['list']==['Cycle 25','Run 40'], b['exConflictApply'])
chk('a duplicate retry does not double it', b['doubleRetry']['op'] is None)
chk('signing out retires every slot', b['signOut']['op'] is None and b['signOut']['fridgeOp'] is None)
chk('fridge retry runs once', b['fridgeRetry']['banana']==['Banana 2'], b['fridgeRetry'])
chk('cancelling the fridge step retires it', b['fridgeCancel']['op'] is None)
# --- round 10: the snapshot is what runs, and a re-read cannot revert later work
chk('fridge add retries its own entry', c['fridgeAddSnapshot']['added']==['Banana 2'], c['fridgeAddSnapshot'])
chk('fridge remove retries its own target', c['fridgeRemoveSnapshot']['left']==['Rye bread 6'], c['fridgeRemoveSnapshot'])
chk('fridge eat retries its own amount', c['fridgeEatSnapshot']['meal'][-1]=='GREEK YOGURT', c['fridgeEatSnapshot'])
chk('re-read keeps work done since', not c['planStaleReread']['lost'] and c['planStaleReread']['replaced'], c['planStaleReread'])
chk('a paused plan hides a held day change', not c['pauseThenReread']['blockedAfterPause'] and c['pauseThenReread']['statusAfterReread']=='paused', c['pauseThenReread'])
chk('re-read with no target changes nothing', c['rereadNoTarget']['plan'] is None)
chk('a shelf that moved refuses the retry', c['eatShortfall']['stale'] and c['eatShortfall']['meals']==0, c['eatShortfall'])
# --- round 11: separate slots, and attempt identity
s=d['separateSlots']
chk('plan and exercise hold separately', s['both']['plan']=='planPause' and s['both']['ex']=='exSave', s['both'])
chk('cancelling the session keeps the plan failure', s['afterCancelEx']['plan']=='planPause' and s['afterCancelEx']['ex'] is None, s['afterCancelEx'])
chk('the plan retry still works afterwards', s['pauseStillWorks']=='paused')
chk('lost on the way reads as not applied', d['lostOnTheWay']['verdict']=='notApplied' and d['lostOnTheWay']['store']==0, d['lostOnTheWay'])
chk('reply lost reads as applied', d['replyLost']['verdict']=='applied' and d['replyLost']['day1']=='skipped' and d['replyLost']['held'] is None, d['replyLost'])
chk('offline says it still cannot tell', d['cannotTell']['first']=='unreachable' and d['cannotTell']['then']=='applied', d['cannotTell'])
chk('bringing it in keeps other changes', d['bringInKeepsOthers']['day0']=='done' and d['bringInKeepsOthers']['day1']=='skipped', d['bringInKeepsOthers'])
chk('a retry keeps the same attempt id', d['retryKeepsId']['first']==d['retryKeepsId']['second']=='try-1' and d['retryKeepsId']['list']==[], d['retryKeepsId'])
chk('an identical earlier session is not this attempt', d['exerciseVerdicts']['lost']=='notApplied' and d['exerciseVerdicts']['landed']=='applied', d['exerciseVerdicts'])
chk('yogurt already on the shelf is not this attempt', d['fridgeVerdicts']['lost']['verdict']=='notApplied' and d['fridgeVerdicts']['landedVerdict']=='applied', d['fridgeVerdicts'])
chk('deleting clears both slots and the store', d['deleteClears']['plan'] is None and d['deleteClears']['ex'] is None and d['deleteClears']['store']==0, d['deleteClears'])
# --- round 12: running a held attempt uses ITS content, on every path
chk('bring-in saves the attempt, not the form', r12['bringInExercise']['saved']==['Walk 30 Moderate'], r12['bringInExercise'])
chk('bring-in keeps the open session draft', r12['bringInExercise']['openDraft']['kind']=='Run' and r12['bringInExercise']['openStep']=='form', r12['bringInExercise'])
chk('bring-in creates the attempt\'s plan', r12['bringInPlan']['days']==7, r12['bringInPlan'])
chk('bring-in keeps the open plan draft', r12['bringInPlan']['openDraftDays']==11, r12['bringInPlan'])
chk('retry still saves its own session', r12['retryExercise']['saved']==['Walk 30 Moderate'], r12['retryExercise'])
chk('retry still creates its own plan', r12['retryPlan']['days']==7 and r12['retryPlan']['openDraftDays']==11, r12['retryPlan'])
chk('an unchanged form is cleared as usual', r12['bringInUnchangedForm']['openDraft'] is None, r12['bringInUnchangedForm'])
chk('bring-in writes once', r12['bringInOnce']['once']==1 and r12['bringInOnce']['after']==1, r12['bringInOnce'])
# --- round 13: a settled conflict still owes a decision; a partial shelf is never assumed
cc = r13['createConflictReread']
chk('create-conflict panel survives the re-read', cc['shownBefore'] and cc['shownAfter'], cc)
chk('re-read still shows the comparison', 'already started on another device' in cc['theirs'] and cc['mine']=='Create the plan.', cc)
chk('no re-apply button for a settled conflict', cc['settled'] and not cc['canReapply'], cc)
chk('keep-latest and drop stay available', cc['keepShown'], cc)
chk('keeping the latest retires the attempt', r13['createConflictKeep']['held'] is None and not r13['createConflictKeep']['shown'], r13['createConflictKeep'])
chk('dropping retires the attempt', r13['createConflictDrop']['held'] is None and not r13['createConflictDrop']['shown'], r13['createConflictDrop'])
chk('a vanished item stops the eat retry', r13['eatItemVanished']['stale'] and r13['eatItemVanished']['meals']==[], r13['eatItemVanished'])
chk('and leaves the remaining stock alone', r13['eatItemVanished']['stock']==['Rye bread 6'], r13['eatItemVanished'])
chk('an intact choice still records both items', 'GREEK YOGURT + RYE BREAD' in r13['eatIntact']['meals'] and not r13['eatIntact']['stale'], r13['eatIntact'])
chk('a shortfall is still caught', r13['eatShortfall']['stale'] and r13['eatShortfall']['meals']==0, r13['eatShortfall'])
chk('a refused retry keeps the original choice', r13['staleKeepsChoice']['stale'] and r13['staleKeepsChoice']['mine']=='0.5 cups Greek yogurt + 1 slice Rye bread', r13['staleKeepsChoice'])
chk('and says what no longer matches', 'no longer on the shelf' in r13['staleKeepsChoice']['trouble'], r13['staleKeepsChoice'])
chk('re-select restores only what survives', r13['staleReselect']['picks']==['Rye bread 1'] and r13['staleReselect']['meals']==0, r13['staleReselect'])
chk('re-select does not confirm anything', r13['staleReselect']['stock']==['Rye bread 6'], r13['staleReselect'])
chk('starting over records nothing', not r13['staleStartOver']['stale'] and r13['staleStartOver']['snap'] is None and r13['staleStartOver']['meals']==0, r13['staleStartOver'])
# --- round 14: cancel is scoped to its step; an add matches on quantity too
chk('cancelling another step keeps the held attempt', r14['cancelOtherStep']['after']=='eat' and r14['cancelOtherStep']['shown'], r14['cancelOtherStep'])
chk('cancelling its own step retires it', r14['cancelOwnStep']['after'] is None and not r14['cancelOwnStep']['shown'], r14['cancelOwnStep'])
chk('cancelling the add form retires the add', r14['cancelAddStep']['after'] is None, r14['cancelAddStep'])
chk('a different quantity is a different attempt', r14['differentQty']['stillHeld']=='add' and r14['differentQty']['shown'], r14['differentQty'])
chk('and the held one can still be checked', r14['differentQtyCheck']['verdict']=='applied', r14['differentQtyCheck'])
chk('the same quantity retires it', r14['sameQty']['after'] is None and not r14['sameQty']['shown'], r14['sameQty'])
chk('a different food never matches', r14['otherFood']['after']=='add', r14['otherFood'])
chk('cancelling another target keeps the held remove', r14['cancelOtherTarget']['after']=='remove' and r14['cancelOtherTarget']['stillYogurt']=='f-yogurt', r14['cancelOtherTarget'])
chk('cancelling its own target retires it', r14['cancelOwnTarget']['after'] is None and not r14['cancelOwnTarget']['shown'], r14['cancelOwnTarget'])
chk('a different eat selection keeps the held one', r14['cancelOtherEat']['after']=='eat', r14['cancelOtherEat'])
chk('a fresh add form keeps the held add', r14['cancelFreshAdd']['after']=='add', r14['cancelFreshAdd'])
total = 72
if fails:
    print('FAILS (%d of %d):' % (len(fails), total)); print(json.dumps(fails, indent=1)); sys.exit(1)
print('all %d checks pass' % total)
