"""Check all generated routes, anchors, citations, solutions and manuscript agreement."""
from pathlib import Path
from lxml import html
from urllib.parse import urlsplit,unquote
import json,re,hashlib
ROOT=Path(__file__).resolve().parents[1];DIST=ROOT/'dist';origin=(ROOT/'site-origin.txt').read_text().strip();manifest=json.loads((DIST/'book-manifest.json').read_text());problems=[];links=0;cache={}
def doc(p):
 if p not in cache:cache[p]=html.fromstring(p.read_text())
 return cache[p]
for p in DIST.rglob('*.html'):
 d=doc(p);ids=d.xpath('//@id')
 if len(ids)!=len(set(ids)):problems.append(str(p.relative_to(DIST))+': duplicate IDs')
 if '::: {.solution}' in d.text_content() or '::::' in d.text_content():problems.append(str(p)+': leaked source fences')
 for node in d.xpath('//*[@href or @src]'):
  href=node.get('href') or node.get('src');u=urlsplit(href)
  if u.scheme and (u.scheme not in ('http','https') or u.netloc!=urlsplit(origin).netloc):continue
  if u.netloc and u.netloc!=urlsplit(origin).netloc:continue
  path=unquote(u.path);target=(DIST/path.lstrip('/')) if path.startswith('/') else (p.parent/path) if path else p
  if target.is_dir() or path.endswith('/'):target=target/'index.html'
  if not target.exists():problems.append(str(p.relative_to(DIST))+': missing '+href);continue
  links+=1
  if u.fragment and target.suffix=='.html' and unquote(u.fragment) not in doc(target).xpath('//@id'):problems.append(str(p.relative_to(DIST))+': missing anchor '+href)
for ch in manifest['chapters']:
 p=next((ROOT/'content').glob(f"{ch['number']:02}-*.md"));assert hashlib.sha256(p.read_bytes()).hexdigest()==ch['sha256']
 d=doc(DIST/ch['url'].lstrip('/')/'index.html');ex=d.xpath('//div[contains(concat(" ",@class," ")," exercise ")]')
 for j,e in enumerate(ex,1):
  if e.get('id')!=f"ex{ch['number']:02}-{j}":problems.append('Wrong exercise numbering in '+ch['id'])
  if len(e.xpath('.//details[contains(@class,"solution")]'))!=1:problems.append('Solution missing for '+e.get('id'))
 for span in d.xpath('//span[contains(@class,"math")]'):
  if not span.xpath('.//math'):problems.append('Unrendered math in '+ch['id'])
for lab in manifest['labs']:
 d=doc(DIST/lab['url'].split('#')[0].lstrip('/')/'index.html')
 if len(d.xpath(f'//*[@id="lab-{lab["key"]}"]//*[@data-lab="{lab["key"]}"]'))!=1:problems.append('Missing lab '+lab['number'])
report={'html_documents':len(cache),'internal_links_checked':links,'chapters':25,'exercises':sum(c['exercises'] for c in manifest['chapters']),'solutions':sum(c['solutions'] for c in manifest['chapters']),'labs':len(manifest['labs']),'citations':manifest['references'],'problems':problems}
print(json.dumps(report,indent=2));raise SystemExit(bool(problems))
