from pathlib import Path
import argparse, csv, hashlib, json, re, subprocess, sys, zipfile
import xml.etree.ElementTree as ET
P=Path(__file__).resolve().parent
arg=argparse.ArgumentParser();arg.add_argument('--ledger');arg.add_argument('--glossary');arg.add_argument('--epub');a=arg.parse_args()
meta=json.loads((P/'lesson.json').read_text());checks=[]
def check(name,condition,detail):
 checks.append({'check':name,'pass':bool(condition),'detail':detail})
def pdf_text(path):
 r=subprocess.run(['pdftotext','-raw',str(path),'-'],capture_output=True,text=True)
 if r.returncode:raise ValueError(r.stderr)
 return r.stdout
def pdf_pages(path):
 r=subprocess.run(['pdfinfo',str(path)],capture_output=True,text=True)
 m=re.search(r'^Pages:\s+(\d+)',r.stdout,re.M)
 if not m:raise ValueError('pdfinfo could not determine page count')
 return int(m.group(1))
try:
 source=P/'source.pdf';text=pdf_text(source)
 check('source_page_count',pdf_pages(source)==meta['pages'],str(pdf_pages(source)))
 normalized=' '.join(text.split())
 check('native_text',all(' '.join(s.split()) in normalized for s in meta['sentences']),str(len(text))+' extracted characters')
 if (P/'scan.pdf').exists():
  scan=pdf_text(P/'scan.pdf');check('scan_has_no_native_text',len(scan.strip())==0,str(len(scan.strip())))
 if meta['kind']=='ocr':
  ocr=(P/'ocr.txt').read_text();check('local_tesseract_numeric_anchors',all(t in ocr for t in ['21.5','0.8','V03']),ocr.strip())
  confusion={'V01': 'VO1' in ocr and 'V01' not in ocr, 'V02': 'VO2' in ocr and 'V02' not in ocr}
  check('ocr_confusion_recorded_for_manual_review',True,{'zero_letter_confusion':confusion,'note':'Known OCR errors are retained; this is not a claim of perfect recognition.'})
 if meta['kind']=='columns':
  check('formula_and_labels',all(s in text for s in ['(1)','(2)','x_bar','abs(y-y_hat)']),text)
 if meta['kind']=='pages':
  path=P/(a.ledger or 'ledger.csv');rows=list(csv.DictReader(path.open()));pages=[int(r['source_page']) for r in rows]
  check('ledger_exact_coverage',sorted(pages)==list(range(1,7)),{'actual':pages,'required':[1,2,3,4,5,6]})
 if meta['kind']=='glossary':
  path=P/(a.glossary or 'glossary.csv');rows=list(csv.DictReader(path.open(encoding='utf-8')))
  check('glossary_columns',bool(rows) and set(rows[0])=={'source','target','tgt_lng'},list(rows[0]) if rows else [])
  found={};conflicts=[]
  for row in rows:
   key=(row['source'].strip(),row['tgt_lng'].strip())
   if key in found and found[key]!=row['target'].strip():conflicts.append(key)
   found[key]=row['target'].strip()
  check('glossary_no_conflicting_terms',not conflicts,conflicts)
  check('source_terms_repeated',all(text.count(term)>=2 for term in ['cooling loop','rate limit','failover route']),{term:text.count(term) for term in ['cooling loop','rate limit','failover route']})
 if meta['kind']=='zotero':
  item=json.loads((P/'item.json').read_text());rows=list(csv.DictReader((P/'attachments.tsv').open(),delimiter='\t'))
  check('original_test_item',len(item)==1 and item[0]['id']=='qr-test-item-01',item[0]['id'])
  check('source_attachment_fixture',len(rows)==1 and rows[0]['item_id']==item[0]['id'] and (P/rows[0]['path']).is_file(),rows)
  check('source_hash_unchanged',hashlib.sha256(source.read_bytes()).hexdigest()==meta['source_sha256'],meta['source_sha256'])
 if meta['kind']=='web':
  body=(P/'article.html').read_text();ids=re.findall(r'<p id="(p\d)">',body)
  check('original_paragraph_ids',ids==['p1','p2','p3','p4'],ids)
  config=json.loads((P/'settings.example.json').read_text())
  check('only_placeholder_config',config['apiKey']=='REPLACE_LOCALLY' and config['model']=='YOUR_AVAILABLE_MODEL', 'No credentials; no model call')
 if meta['kind']=='epub':
  path=Path(a.epub) if a.epub else P/'source.epub'
  with zipfile.ZipFile(path) as z:
   names=z.namelist();first=z.infolist()[0]
   check('epub_mimetype_first_uncompressed',first.filename=='mimetype' and first.compress_type==0 and z.read('mimetype')==b'application/epub+zip',first.filename)
   container=ET.fromstring(z.read('META-INF/container.xml'));rf=container.find('.//{*}rootfile');opfpath=rf.attrib['full-path'];opf=ET.fromstring(z.read(opfpath));base=Path(opfpath).parent
   manifest={i.attrib['id']:i.attrib['href'] for i in opf.findall('.//{*}manifest/{*}item')}
   spine=[i.attrib['idref'] for i in opf.findall('.//{*}spine/{*}itemref')]
   check('spine_resources_exist',bool(spine) and all((base/manifest[i]).as_posix() in names for i in spine),spine)
   navitems=[i for i in opf.findall('.//{*}manifest/{*}item') if 'nav' in i.attrib.get('properties','').split()]
   check('epub_nav_exists',len(navitems)==1,len(navitems))
   if navitems:
    navpath=base/navitems[0].attrib['href'];nav=ET.fromstring(z.read(navpath.as_posix()));hrefs=[e.attrib['href'].split('#')[0] for e in nav.findall('.//{*}a')]
    check('nav_targets_exist',bool(hrefs) and all((navpath.parent/h).as_posix() in names for h in hrefs),hrefs)
   if not a.epub:check('original_three_chapters',len(spine)==3,len(spine))
 if meta['kind']=='retry':
  log=json.loads((P/'retry-log.json').read_text());check('real_local_http_retry',[r['status'] for r in log['requests']]==[429,200] and log['fixture'] is True,log)
except Exception as e:check('runtime_exception',False,str(e))
result={'fixture_only':True,'translation_model_executed':False,'checks':checks,'passed':all(c['pass'] for c in checks)}
print(json.dumps(result,ensure_ascii=False,indent=2));sys.exit(0 if result['passed'] else 1)
