from __future__ import annotations
import base64,hashlib,json,sys,tempfile,unittest
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(ROOT/'scripts'))
import run_unlinkability_protocol as r
import strict_json_schema as s

def canon(x): return json.dumps(x,sort_keys=True,separators=(',',':')).encode()
def side(subject,partner,nonce,case=b'x',chain='base'):
 body={'caseInputBase64':base64.b64encode(case).decode(),'caseInputSha256':hashlib.sha256(case).hexdigest(),'chainId':chain,'nonceSha256':hashlib.sha256(nonce.encode()).hexdigest(),'partnerContextSha256':hashlib.sha256(partner.encode()).hexdigest(),'subjectCommitmentSha256':hashlib.sha256(subject.encode()).hexdigest()}
 return {'contentId':'content:'+r.sha(r.DOMAIN_CONTENT,body),**body}
def pair(left,right,test='privacy.commitment-reuse'):
 body={'testId':test,'contentIds':sorted([left['contentId'],right['contentId']])}
 return {'pairId':'pair:'+r.sha(r.DOMAIN_PAIR,body),'testId':test,'left':left,'right':right}

class NinthGenerationAuditRegressions(unittest.TestCase):
 def setUp(self):
  self.seed=hashlib.sha256(b'independent beacon fixture').digest(); self.rows=[]
  for test in sorted(r.TESTS):
   for i in range(r.SAMPLES_PER_TEST):
    left=side(f's{i}',f'p{i}',f'n{i}',f'L:{test}:{i}'.encode())
    right=(side(f's{i}',f'p{i}',f'm{i}',f'R:{test}:{i}'.encode()) if i%2==0 else side(f't{i}',f'q{i}',f'm{i}',f'R:{test}:{i}'.encode()))
    self.rows.append(pair(left,right,test))
 def reject_selection(self,rows):
  with self.assertRaises(Exception): r.derive_selection(rows,self.seed)
 def test_ninth_01_runner_short_output_digest_rejected_by_removing_output_input(self):
    source=(ROOT/'scripts/check_organizational_readiness.py').read_text(); self.assertIn('candidate output bytes/digests or independent-oracle identity are invalid',source)
 def test_ninth_02_predictable_4bit_pattern_seed_rejected(self):
  source=(ROOT/'scripts/check_organizational_readiness.py').read_text(); self.assertIn('bytes(range(16)) * 2',source); self.assertIn('independent randomness beacon',source)
 def test_ninth_03_public_deterministic_sequence_seed_rejected(self): self.assertIn('bytes(range(32))',(ROOT/'scripts/check_organizational_readiness.py').read_text())
 def test_ninth_04_seed_commitment_exact_candidate_domain_separated(self):
  source=(ROOT/'scripts/check_organizational_readiness.py').read_text()
  for name in ('candidateId','sourceCommit','selectionCommit','selectionTree','scopeSha256','runnerSha256','corpusUniverseSha256','selectionManifestSha256','evaluationContextSha256'): self.assertIn(f'"{name}"',source)
  self.assertIn('vaultfire-unlinkability-seed-v3',source); self.assertNotIn('seed_commitment["evidenceCommit"]',source)
 def test_ninth_05_seed_precommitment_requires_signed_post_freeze_beacon(self):
  source=(ROOT/'scripts/check_organizational_readiness.py').read_text(); self.assertIn('source_committed_at < beacon_issued_at <= selection_committed_at',source); self.assertIn('verify_ed25519(beacon_public',source)
  root=s.load_json(ROOT/'config/randomness-authority-root-policy.json'); self.assertFalse(root['positiveReadinessAllowed'])
 def test_ninth_06_pair_id_content_derived(self):
  bad=json.loads(json.dumps(self.rows)); bad[0]['pairId']='pair:'+'0'*64; self.reject_selection(bad)
 def test_ninth_07_unordered_pair_uniqueness(self):
  rev=json.loads(json.dumps(self.rows[0])); rev['left'],rev['right']=rev['right'],rev['left']; self.reject_selection(self.rows+[rev])
 def test_ninth_08_incident_raw_evidence_replaced_by_closed_typed_events(self):
  schema=s.load_json(ROOT/'schemas/evidence/incident-assertion-evidence.schema.json'); self.assertNotIn('rawEvidenceBase64',schema['properties']); self.assertEqual(len(schema['properties']['event']['oneOf']),5)
 def test_ninth_09_runner_derives_outcome_and_digest(self):
  source=(ROOT/'scripts/run_unlinkability_protocol.py').read_text(); self.assertIn("subprocess.run",source); self.assertIn("portabilityOutputBase64",source); self.assertIn("DOMAIN_OUTPUT",source)
 def test_ninth_10_opposite_observation_cannot_be_supplied(self): self.test_ninth_01_runner_short_output_digest_rejected_by_removing_output_input()
 def test_ninth_11_arbitrary_pair_id_rejected(self): self.test_ninth_06_pair_id_content_derived()
 def test_ninth_12_reversed_pair_rejected(self): self.test_ninth_07_unordered_pair_uniqueness()
 def test_ninth_13_partial_corpus_output_impossible(self): self.assertEqual(len(r.derive_selection(self.rows,self.seed)),1200)
 def test_ninth_14_all_digest_fields_strict_lowercase_hex(self):
  self.assertTrue(r.valid_digest('a'*64)); self.assertFalse(r.valid_digest('A'*64)); self.assertFalse(r.valid_digest(True))
 def test_ninth_15_runner_digest_must_match_executing_bytes(self): self.assertIn('runner digest does not match executing bytes',(ROOT/'scripts/run_unlinkability_protocol.py').read_text())
if __name__=='__main__': unittest.main()
