from pathlib import Path
import pcbnew,wx,json,xml.etree.ElementTree as E,math
app=wx.App(False);quiet=wx.LogNull();R=Path(__file__).resolve().parent;D=R/'cad';b=pcbnew.LoadBoard(str(D/'vacuum_pressure_a114.kicad_pcb'))
expected={'J1':{'1':'/CTRL_3V3','2':'/GND','3':'/PANEL_SCL','4':'/PANEL_SDA'},'U1':{'1':None,'2':'/CTRL_3V3','3':None,'4':None,'5':None,'6':'/PANEL_SDA','7':'/PANEL_SCL','8':'/GND'},'C1':{'1':'/CTRL_3V3','2':'/GND'}}
root=E.parse(D/'netlist.xml').getroot();xml={(x.get('ref'),x.get('pin')):n.get('name') for n in root.findall('./nets/net') for x in n.findall('node')};pads={};geom=[]
mm=pcbnew.ToMM;xy=lambda p:[mm(p.x),mm(p.y)]
for f in b.GetFootprints():
 assert f.GetReference() in expected
 for p in f.Pads():
  key=(f.GetReference(),p.GetNumber());assert key not in pads;pads[key]=p
  want=expected[key[0]][key[1]];actual=p.GetNetname();assert actual==xml[key]
  assert actual==want if want is not None else actual.startswith('unconnected-')
  if key[0]=='U1':
   coords={'1':[-4.98,-3.81],'2':[-4.98,-1.27],'3':[-4.98,1.27],'4':[-4.98,3.81],'5':[4.98,3.81],'6':[4.98,1.27],'7':[4.98,-1.27],'8':[4.98,-3.81]}
   q=xy(p.GetPosition());offset=[q[0]-20,q[1]-12]
   assert all(abs(a-z)<1e-6 for a,z in zip(offset,coords[key[1]]))
   assert all(abs(a-z)<1e-6 for a,z in zip(xy(p.GetSize()),[2,.9]))
   assert p.GetAttribute()==pcbnew.PAD_ATTRIB_SMD
  if key[0]=='J1':assert abs(mm(p.GetDrillSize().x)-1.2)<1e-6
  geom.append({'ref':key[0],'pin':key[1],'net':actual,'xy':xy(p.GetPosition()),'size':xy(p.GetSize()),'hole':mm(p.GetDrillSize().x)})
assert len(pads)==14 and len(list(b.GetFootprints()))==3
for t in b.GetTracks():assert not t.GetNetname().startswith('unconnected-')
assert b.GetCopperLayerCount()==2
bbox=b.GetBoardEdgesBoundingBox();assert abs(mm(bbox.GetWidth())-30.05)<.001 and abs(mm(bbox.GetHeight())-25.05)<.001
main=pcbnew.LoadBoard(str(R/'baseline/A111-mainboard.kicad_pcb'))
if main:
 j=next(f for f in main.GetFootprints() if f.GetReference()=='J5')
 for p in j.Pads():assert p.GetNetname()==expected['J1'][p.GetNumber()]
 # External up-pull resistors must not be accidentally added on this adapter.
 for ref,net in [('R18','/PANEL_SCL'),('R19','/PANEL_SDA')]:
  f=next(f for f in main.GetFootprints() if f.GetReference()==ref);assert {p.GetNetname() for p in f.Pads()}=={'/CTRL_3V3',net}
tracks=[]
for t in b.GetTracks():tracks.append({'net':t.GetNetname(),'start':xy(t.GetStart()),'end':xy(t.GetEnd()),'width':mm(t.GetWidth(t.GetLayer()) if isinstance(t,pcbnew.PCB_VIA) else t.GetWidth()),'via':isinstance(t,pcbnew.PCB_VIA),'layer':t.GetLayerName()})
(R/'saved_geometry.json').write_text(json.dumps({'pads':geom,'tracks':tracks},indent=2))
(R/'saved_board_checks.json').write_text(json.dumps({'pads_checked':14,'fitted_parts':3,'NC_pins_isolated':[1,3,4,5],'candidate_land_checked':True,'A111_header_checked':bool(main),'native_copper_objects':len(tracks),'dimensions_mm':[30,25],'physical_assembly_verified':False},indent=2))
print('PASS:14 saved pads / independent pin map / XML / candidate land / NC isolation; A111 header checked:',bool(main))
