"""Run three reproducible internal exercises on fictional data, without network access."""
from pathlib import Path
from html.parser import HTMLParser
from tempfile import TemporaryDirectory
import argparse, csv, hashlib, io, json, os, platform, tarfile

def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()

def reconcile(texts):
    result={}
    for text in texts:
        for row in csv.DictReader(io.StringIO(text)):
            assert row['id'].strip(), 'Un identifiant est requis'
            result[row['id']]=row
    return list(result.values())

source_a='id,statut,action\nD-101,envoye,\nD-102,accepte,livrer\n'
source_b='id,statut,action\nD-101,envoye,relancer\nD-103,envoye,\n'
initial=reconcile([source_a,source_b])
updated=reconcile([source_a,source_b+'D-104,envoye,\n'])
missing=lambda rows:[row['id']for row in rows if row['statut']=='envoye' and not row['action']]
assert len(initial)==3 and missing(initial)==['D-103']
assert len(updated)==4 and missing(updated)==['D-103','D-104']
invalid_detected=False
try:reconcile(['id,statut,action\n,envoye,\n'])
except AssertionError:invalid_detected=True
assert invalid_detected

with TemporaryDirectory(prefix='hexalocal-lab-') as temp:
    root=Path(temp);original=root/'original';original.mkdir();restored=root/'restored';restored.mkdir()
    (original/'regles.json').write_text('{"donnees":"fictives","seuil":5}\n')
    (original/'suivi.csv').write_text('article,stock\ncable,3\n')
    for file in original.iterdir():os.chmod(file,0o640)
    expected={file.name:sha(file)for file in original.iterdir()}
    archive=root/'copie.tgz'
    with tarfile.open(archive,'w:gz')as tar:
        for file in original.iterdir():tar.add(file,arcname=file.name)
    def restore():
        with tarfile.open(archive,'r:gz')as tar:
            for name in expected:
                member=tar.getmember(name);assert member.isfile() and Path(member.name).name==name
                stream=tar.extractfile(member);assert stream is not None
                target=restored/name;target.write_bytes(stream.read());os.chmod(target,member.mode&0o777)
    restore()
    assert all(sha(restored/name)==value for name,value in expected.items())
    assert all((restored/name).stat().st_mode&0o777==0o640 for name in expected)
    (restored/'regles.json').write_text('modification fictive\n')
    alteration_detected=sha(restored/'regles.json')!=expected['regles.json'];assert alteration_detected
    restore();assert all(sha(restored/name)==value for name,value in expected.items())

class Page(HTMLParser):
    def __init__(self):super().__init__();self.title=False;self.description=False;self.canonical=False
    def handle_starttag(self,tag,attrs):
        a=dict(attrs)
        if tag=='title':self.title=True
        if tag=='meta' and a.get('name')=='description' and a.get('content'):self.description=True
        if tag=='link' and a.get('rel')=='canonical' and a.get('href','').startswith('https://'):self.canonical=True
    def missing(self):return [key for key in ['title','description','canonical']if not getattr(self,key)]
before=Page();before.feed('<html lang="fr"><body><h1>La page de démonstration</h1></body></html>')
after=Page();after.feed('<html lang="fr"><head><title>Page de démonstration : un besoin précis</title><meta name="description" content="Une description propre à cette page fictive."><link rel="canonical" href="https://example.invalid/besoin-precis"></head><body><h1>La page de démonstration</h1></body></html>')
assert before.missing()==['title','description','canonical'] and not after.missing()

report={
    'schemaVersion':1,'date':'2026-11-03','environment':'Python '+platform.python_version()+', '+platform.system(),
    'scope':'Démonstrations internes réellement exécutées, exclusivement sur des données et une page fictives. Aucun compte client, email, paiement ou service externe.',
    'automation':{'inputRows':4,'uniqueQuotes':len(initial),'missingActions':missing(initial),'afterNewRow':{'uniqueQuotes':len(updated),'missingActions':missing(updated)},'invalidIdDetected':invalid_detected,'rule':'Rapprocher par identifiant, conserver la dernière ligne, signaler un devis envoyé sans action.'},
    'restoration':{'files':list(expected),'restoredFiles':2,'matchingHashes':2,'permissionsVerified':'0640','alterationDetected':alteration_detected,'secondRestoreVerified':True,'scope':'Archive et restauration dans des dossiers temporaires distincts ; aucune donnée du VPS de production copiée.'},
    'seo':{'before':before.missing(),'after':after.missing(),'scope':'Page HTML fictive. Présence des trois balises contrôlée ; aucun moteur de recherche interrogé, aucune indexation ou position mesurée.'},
    'checksPassed':True,
}
parser=argparse.ArgumentParser();parser.add_argument('--output',type=Path,required=True);args=parser.parse_args()
args.output.parent.mkdir(parents=True,exist_ok=True);args.output.write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf8')
print(json.dumps({'checksPassed':True,'exercises':3,'networkCalls':0,'realEmails':0}))
