import pcbnew as k, wx, json, hashlib
from pathlib import Path
from collections import Counter
import xml.etree.ElementTree as E
P=Path(__file__).resolve().parent
app=wx.App(False); quiet=wx.LogNull()
b=k.LoadBoard(str(P/'baseline-A82.kicad_pcb'))
c=k.LoadBoard(str(P/'drill_motor_a85_cutoff.kicad_pcb'))
def geom(t):
 if isinstance(t,k.PCB_VIA): return ('via',t.GetNetname(),t.GetPosition().x,t.GetPosition().y,t.GetWidth(k.F_Cu),t.GetDrillValue())
 return ('track',t.GetNetname(),*sorted([(t.GetStart().x,t.GetStart().y),(t.GetEnd().x,t.GetEnd().y)]),t.GetWidth(),t.GetLayer())
a=Counter(geom(t) for t in b.GetTracks() if t.GetNetname()!='/DRV_RUN_REQ')
z=Counter(geom(t) for t in c.GetTracks())
assert sum(a.values())==654 and not a-z
old={f.GetReference():f for f in b.GetFootprints()}; new={f.GetReference():f for f in c.GetFootprints()}
def padgeom(f):
 return sorted((p.GetNumber(),p.GetPosition().x,p.GetPosition().y,p.GetSize().x,p.GetSize().y,int(p.GetShape()),p.GetDrillSize().x,p.GetDrillSize().y) for p in f.Pads())
for ref,f in old.items():
 assert padgeom(f)==padgeom(new[ref]),ref
 for pad in f.Pads():
  target=next(p for p in new[ref].Pads() if p.GetNumber()==pad.GetNumber())
  expected='/DRV_RUN_GATED' if ref=='R5' and pad.GetNumber()=='1' else pad.GetNetname()
  assert target.GetNetname()==expected,(ref,pad.GetNumber())
def outline(board):
 return sorted((int(d.GetShape()),d.GetStart().x,d.GetStart().y,d.GetEnd().x,d.GetEnd().y,d.GetWidth()) for d in board.GetDrawings() if d.GetLayer()==k.Edge_Cuts)
assert outline(b)==outline(c)
root=E.parse(P/'netlist-A85.xml').getroot()
expected={(x.get('ref'),x.get('pin')):n.get('name') for n in root.findall('nets/net') for x in n.findall('node')}
actual={(f.GetReference(),p.GetNumber()):p.GetNetname() for f in c.GetFootprints() for p in f.Pads() if f.GetReference() not in ['MH1','MH2','MH3','MH4']}
assert actual==expected, {'missing':list(expected.keys()-actual.keys()),'extra':list(actual.keys()-expected.keys()),'wrong':[str(x) for x in expected.keys()&actual.keys() if expected[x]!=actual[x]]}
assert len(new.keys()-old.keys())==29
assert str(new['U5'].GetFPID().GetLibItemName())=='TI_DCT0008A_SSOP8_3x3mm_P0.65mm'
assert new['U5'].GetValue()=='SN74LVC1G74DCTR'
r=json.loads((P/'final-DRC-A86.json').read_text())
assert not r['violations'] and not r['unconnected_items']
parity=r['schematic_parity']
assert len(parity)==4 and all(x['type']=='extra_footprint' and len(x['items'])==1 and x['items'][0]['description'] in ['Footprint MH1','Footprint MH2','Footprint MH3','Footprint MH4'] for x in parity)
result={'status':'NATIVE_PCB_LOCAL_REVIEW_PASS_NOT_FABRICATION_RELEASE','original_non_RAW_copper_retained_exact':sum(a.values()),'current_copper_objects':sum(z.values()),'original_footprints_pad_geometry_preserved':len(old),'added_components':29,'native_netlist_pad_assignments_checked':len(actual),'outline_unchanged':True,'native_DRC_violations':0,'native_unconnected':0,'preexisting_mounting_hole_parity':4,'visual_review':'board-preview-A86.png viewed; 180x80mm engineering carrier; not final product fit','U5_MPN':'SN74LVC1G74DCTR','footprint_source':'TI DCT0008A 4220784/D 10/2025; 1.1x0.4mm lands, 0.65mm pitch, 3.8mm row centers','rule_relaxation':False,'physical_validation':False,'manufacturing_release':False,'motor_run_permission':False,'source_sha256':{name:hashlib.sha256((P/name).read_bytes()).hexdigest() for name in ['drill_motor_a85_cutoff.kicad_pcb','drill_motor_a85_cutoff.kicad_sch','drill_motor_a85_cutoff.kicad_pro','netlist-A85.xml']}}
(P/'board-verification-A86.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result))
