import hashlib,json,pathlib,copy
import numpy as np
from task import oracle,permutations,episode,seed
from learner import Model,run_episode
from builder import build
R=pathlib.Path(__file__).resolve().parent

def main():
    checks=[];errors={}
    def check(value,label):
        if not value:raise AssertionError(label)
        checks.append(label)
    pi=((1,0),(0,1));e=[{'type':'QUERY','c':0,'j':0},{'type':'WRITE','a':1,'v':1},{'type':'QUERY','c':0,'j':0},{'type':'WRITE','a':1,'v':-1},{'type':'QUERY','c':1,'j':1}]
    a,bank=oracle(e,pi,2);check(a==[0,None,1,None,-1],'hand overwrite/permutation/unwritten answers')
    carry,_=oracle([{'type':'QUERY','c':0,'j':0}],pi,2,bank);reset,_=oracle([{'type':'QUERY','c':0,'j':0}],pi,2);check(carry==[-1] and reset==[0],'oracle carry/reset')
    delayed=[{'type':'WRITE','a':0,'v':1}]+[{'type':'WRITE','a':1,'v':-1}]*40+[{'type':'QUERY','c':1,'j':0}];check(oracle(delayed,pi,2)[0][-1]==1,'hand 41-event delayed query')
    for K in [2,3,4]:
        for C in [2,3,4]:
            p=permutations(K,C,seed('gates',0,'pi',K*10+C));events,answers,meta=episode(K,C,p,seed('gates',0,'events',K*10+C));check(len(events)==128 and meta['queries']>=64 and all(1<=e['delay']<=64 for e in events if e['type']=='QUERY'),f'128 event quota/delay/schema K{K} C{C}')
    parent=Model(seed=17)
    for bad in [{'type':'QUERY','c':4,'j':0},{'type':'WRITE','a':4,'v':1}]:
        try:parent.step(bad,4,4)
        except ValueError:pass
        else:raise AssertionError('inactive position accepted')
    check(True,'inactive schema positions rejected')
    check(all(build(m,r,{'address_positions':8,'context_positions':8,'clock':'external-step'},5)['costs']['execution_scalar_edges']<=1024 for m,r in [(2,3),(8,8),(12,12)]),'pure independently parameterized widths and costs')
    p=permutations(4,4,3);events,answers,_=episode(4,4,p,4)
    def retention(live):
        old=Model(seed=17)
        if live:
            for e in events[:61]:old.step(e,4,4)
        child=old.grow(23);maximum=0.;stateerror=0.
        for e in (events[61:] if live else events):
            y=old.step(e,4,4)[0];z=child.step(e,4,4)[0];maximum=max(maximum,abs(y-z));stateerror=max(stateerror,float(np.max(np.abs(old.branches[0].h-child.branches[0].h))),float(np.max(np.abs(old.branches[0].delay-child.branches[0].delay))))
        check(maximum<=1e-12 and stateerror<=1e-12 and old.tick==child.tick,'live retention' if live else 'reset retention');return maximum,stateerror
    errors['reset_output'],errors['reset_state']=retention(False);errors['carry_output'],errors['carry_state']=retention(True)
    for e in events[:61]:parent.step(e,4,4)
    child=parent.grow(23);child.branches[0].delay[:]=0
    try:child.step({'type':'QUERY','c':0,'j':0},4,4)
    except ValueError:check(True,'broken delay-buffer transfer rejected')
    else:raise AssertionError('broken delay transfer accepted')
    child=parent.grow(23);child.branches[0].h[:]=0;child.branches[0].delay[:]=0
    errors['broken_state']=max(abs(parent.snapshot().step({'type':'QUERY','c':c,'j':j},4,4)[0]-child.snapshot().step({'type':'QUERY','c':c,'j':j},4,4)[0]) for c in range(4) for j in range(4));check(errors['broken_state']>1e-12,'erased carried-state counterexample detected')
    child=parent.grow(23);child.step({'type':'WRITE','a':0,'v':1},4,4);y,x,phi=child.step({'type':'QUERY','c':0,'j':0},4,4);branch=child.branches[1];g=branch.grads(x,phi[1],y-1)
    check(float(np.linalg.norm(g['V']))>0 and np.max(np.abs(g['W']))==0,'zero-readout nonzero gradient; features initially zero gradient')
    branch.V-=.08*g['V'];y,x,phi=child.step({'type':'QUERY','c':0,'j':0},4,4);g=branch.grads(x,phi[1],y-1);check(float(np.linalg.norm(g['W']))>0,'feature-producing weights gain gradient after readout update')
    maxerr=0.;eps=1e-6
    for k,index in [('V',(0,0)),('W',(0,0)),('b',(0,))]:
        parameter=getattr(branch,k);original=parameter[index];vals=[]
        for delta in [eps,-eps]:
            parameter[index]=original+delta;prediction=child.snapshot().step({'type':'QUERY','c':0,'j':0},4,4)[0];vals.append(.5*(prediction-1)**2)
        parameter[index]=original;fd=(vals[0]-vals[1])/(2*eps);maxerr=max(maxerr,abs(fd-g[k][index]))
    errors['finite_difference_gradient']=maxerr;check(maxerr<1e-8,'explicit analytic gradient finite-difference agreement')
    learner=Model(seed=2);initial=run_episode(learner,events,answers,4,4)['loss']
    for _ in range(4):run_episode(learner,events,answers,4,4,True)
    after=run_episode(learner,events,answers,4,4)['loss'];check(after<initial,'calibration-only learner loss falls; no scored feedback')
    result={'status':'passed','checks':checks,'checks_passed':len(checks),'maximum_errors':errors,'calibration_loss_before':initial,'calibration_loss_after':after,'calibration_training_events':512,'calibration_mac_equivalents':4*128*Model(seed=2).branches[0].cost(True),'source_hashes':{n:hashlib.sha256((R/n).read_bytes()).hexdigest() for n in ['gates.py','builder.py','learner.py','task.py','frozen.json','amendment.md']},'claim':'Engineering only. No developmental advantage or held-out size evidence.'}
    (R/'gate_results.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result))
if __name__=='__main__':main()
