# SPDX-License-Identifier: MIT
"""Structural regressions for third independent audit H-02/H-03/M-01."""
import copy, hashlib, json, shutil, subprocess, sys, tempfile, unittest
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1];REPO=ROOT.parent
sys.path.insert(0,str(ROOT/'tools'))
import check
from conform import Failure, build_report, digest, load_json, run_vectors, validate_document

class ThirdAuditRegressions(unittest.TestCase):
 @classmethod
 def setUpClass(cls):
  cls.p=load_json(ROOT/'vectors/positive.json');cls.a=load_json(ROOT/'vectors/adversarial.json');cls.by={x['id']:x for x in cls.p['vectors']}
 def reject(self,d):
  with self.assertRaises(Failure):validate_document(d)
 def bond(self):return copy.deepcopy(self.by['P-ACCOUNTABILITY']['document'])
 def test_H02_self_and_multi_node_cycles_rejected(self):
  d=self.bond();n=d['provenanceGraph']['nodes'][0];n['parentDigests']=[n['digest']];n['derivation']={'kind':'canonical-transform','algorithm':'sha256-rfc8785','parametersDigest':'0x'+'12'*32};d['provenanceGraph']['graphDigest']=digest(d['provenanceGraph']['nodes']);self.reject(d)
  d=self.bond();root=d['provenanceGraph']['nodes'][0];other=copy.deepcopy(root);other['digest']='0x'+'98'*32;other['sourceBinding']['contentDigest']=other['digest'];root['parentDigests']=[other['digest']];other['parentDigests']=[root['digest']]
  for n in (root,other):n['derivation']={'kind':'canonical-transform','algorithm':'sha256-rfc8785','parametersDigest':'0x'+'12'*32}
  d['provenanceGraph']['nodes'].append(other);d['provenanceGraph']['graphDigest']=digest(d['provenanceGraph']['nodes']);self.reject(d)
 def test_H02_generic_unreachable_and_opaque_sources_rejected(self):
  d=self.bond();d['provenanceGraph']['nodes'][0]['sourceClass']='objective-protocol';self.reject(d)
  d=self.bond();other=copy.deepcopy(d['provenanceGraph']['nodes'][0]);other['digest']='0x'+'98'*32;other['sourceBinding']['contentDigest']=other['digest'];d['provenanceGraph']['nodes'].append(other);d['provenanceGraph']['graphDigest']=digest(d['provenanceGraph']['nodes']);self.reject(d)
  for text in ('sha256:biometric-profile-surveillance-derived','behavioral scoring','opaque transitive commitment'):
   d=self.bond();d['extensions']['x-note']={'securityCritical':False,'decisionInfluence':'none','value':text};self.reject(d)
 def test_H02_typed_derivation_is_rooted_and_content_bound(self):
  d=self.bond();root=d['provenanceGraph']['nodes'][0];derived=copy.deepcopy(root);derived['digest']='0x'+'98'*32;derived['parentDigests']=[root['digest']];derived['sourceBinding']['contentDigest']=derived['digest'];derived['derivation']={'kind':'canonical-transform','algorithm':'sha256-rfc8785','parametersDigest':'0x'+'12'*32};d['provenanceGraph']['nodes'].append(derived);d['decisionInputs'][0]['evidenceDigest']=derived['digest'];d['provenanceGraph']['graphDigest']=digest(d['provenanceGraph']['nodes']);self.assertTrue(validate_document(d))
 def test_H03_split_candidate_binding_rejected(self):
  report=build_report(run_vectors({'positive':self.p,'adversarial':self.a}));report['candidate']['canonicalMappingSha256']='00'*32
  with self.assertRaises(Failure):validate_document(report,production_current_time=True)
 def test_M01_caches_excluded_normally_and_rejected_for_release(self):
  normal={p for p,_ in check.artifact_entries()};self.assertFalse(any('.pytest_cache' in p or '__pycache__' in p for p in normal))
  if (ROOT/'.pytest_cache').exists():
   with self.assertRaises(Failure):check.artifact_entries(release_mode=True)
 def test_H03_isolated_commit_cli_finalizes_without_rewrite(self):
  with tempfile.TemporaryDirectory() as td:
   tr=Path(td);pkg=tr/'v3-open-spec';shutil.copytree(ROOT,pkg,ignore=shutil.ignore_patterns('.pytest_cache','__pycache__','*.pyc','*.pyo'));shutil.copy2(REPO/'LICENSE',tr/'LICENSE')
   mapping=load_json(ROOT/'mappings/canonical-v3.json');paths={x['path'] for x in mapping['supportedSourceModules']}|{x['path'] for x in mapping['canonicalSources'].values()}|{'v3-migration/completeness/supported-v3-modules.json','pr7/generated/route-registry.json','pr7/generated/openapi.json'}
   for rel in paths:(tr/rel).parent.mkdir(parents=True,exist_ok=True);shutil.copy2(REPO/rel,tr/rel)
   for rel in ('mappings/canonical-v3.json','interfaces/canonical-v3-interfaces.v1.json'):
    p=pkg/rel;data=json.loads(p.read_text())
    for item in data['supportedSourceModules']:item['sha256']=hashlib.sha256((tr/item['path']).read_bytes()).hexdigest()
    p.write_text(json.dumps(data,indent=2,sort_keys=True)+'\n')
   subprocess.run([sys.executable,'-c',"import sys;sys.path.insert(0,'v3-open-spec/tools');import check;open('v3-open-spec/ARTIFACTS.sha256','w').write(check.expected_manifest())"],cwd=tr,check=True)
   before={p.relative_to(pkg).as_posix():hashlib.sha256(p.read_bytes()).hexdigest() for p in pkg.rglob('*') if p.is_file() and '.pytest_cache' not in p.parts and '__pycache__' not in p.parts}
   subprocess.run(['git','init','-q'],cwd=tr,check=True);subprocess.run(['git','config','user.name','VF3 Fixture'],cwd=tr,check=True);subprocess.run(['git','config','user.email','fixture@example.invalid'],cwd=tr,check=True);subprocess.run(['git','add','.'],cwd=tr,check=True);subprocess.run(['git','commit','-q','-m','fixture'],cwd=tr,check=True)
   run=subprocess.run([sys.executable,str(pkg/'tools/conform.py')],cwd=tr,text=True,capture_output=True)
   after={p.relative_to(pkg).as_posix():hashlib.sha256(p.read_bytes()).hexdigest() for p in pkg.rglob('*') if p.is_file() and '.pytest_cache' not in p.parts and '__pycache__' not in p.parts}
   self.assertEqual(run.returncode,0,run.stderr);self.assertTrue(json.loads(run.stdout)['summary']['conformant']);self.assertEqual(before,after)

if __name__=='__main__':unittest.main()
