"""Offline tests of release policy and failure gates. No services or paid calls."""
import copy,importlib.machinery,importlib.util,json,sys,tempfile,unittest
from pathlib import Path
from unittest.mock import patch
loader=importlib.machinery.SourceFileLoader('animal_cli',str(Path(__file__).with_name('forge-realistic-animal')))
spec=importlib.util.spec_from_loader(loader.name,loader);cli=importlib.util.module_from_spec(spec);loader.exec_module(cli)
class Gates(unittest.TestCase):
 def setUp(self):
  self.tmp=tempfile.TemporaryDirectory(prefix='animal-tests-',dir=Path.cwd());self.d=Path(self.tmp.name);(self.d/'candidate.glb').write_bytes(b'candidate');(self.d/'source.glb').write_bytes(b'source')
  for name in ['rear','belly','side','tail-clips','tail-close','bellyClay','rearClay']:(self.d/('final-'+name+'.png')).write_bytes(b'fixture')
  self.r={'triangles':14000,'browser_errors':[],'errors':[],'bones':['Tail1','Head'],'height_m':1.,'tail':{'region_method':'exported geometric _TAIL_REGION','tail_vertices':100,'max_tail_on_non_tail':0.,'max_leg_on_tail':0.},'contact':{k:{'lowest_surface_m':0} for k in ['walk','run','attack','die']},'foot_contact':{k:{'max_abs_sole_height_m':.001,'max_stance_slip_rms_m':.002,'feet':[{'samples':5}]*4} for k in ['walk','run']},'speeds':{'walk':{'speed':1},'run':{'speed':3}},'clips':[{'name':n,'duration':1,'poses_played':21} for n in ['Idle','Walk','Run','Attack','Hit','Death']]}
  self.src={'height_m':1.,'bones':{'Tail1':{},'Head':{}},'clip_durations':{c['name']:1 for c in self.r['clips']}}
  cli.write(self.d/'fragments.json',{'pass':True})
  self.tail={'fragments_after':{'pass':True},'meshes':[{'boundary_edges_before':0,'boundary_edges_after':0}]};self.audit={'findings':[],'zero_area_triangles':0,'nonfinite_attribute_components':0}
 def tearDown(self):self.tmp.cleanup()
 def gate(self):
  for name,obj in [('final-runtime.json',self.r),('input.json',self.src),('tail.json',self.tail),('audit.json',self.audit)]:cli.write(self.d/name,obj)
  return cli.gates(self.d)
 def test_fragments_fail(self):
  cli.write(self.d/'fragments.json',{'pass':False});self.assertFalse(self.gate()['checks']['fragments'])
 def test_missing_render_fails(self):
  (self.d/'final-belly.png').unlink();self.assertFalse(self.gate()['checks']['evidence'])
 def test_valid(self):self.assertTrue(self.gate()['numeric_pass'])
 def test_corruption_matrix(self):
  changes=[('size',lambda:(self.d/'candidate.glb').write_bytes(b'x'*1500001)),('triangles',lambda:self.r.update(triangles=5999)),('browser',lambda:self.r['browser_errors'].append('load failed')),('clips',lambda:self.r['clips'][0].update(duration=2)),('tail_region_present',lambda:self.r['tail'].update(region_method='legacy weight guess')),('tail_weights',lambda:self.r['tail'].update(max_leg_on_tail=.001)),('ground',lambda:self.r['contact']['run'].update(lowest_surface_m=-.021)),('foot_contact',lambda:self.r['foot_contact']['walk'].update(max_abs_sole_height_m=.03)),('speeds',lambda:self.r['speeds']['run'].update(speed=None)),('geometry',lambda:self.audit.update(findings=['bad'])),('scale',lambda:self.r.update(height_m=2)),('skeleton',lambda:self.r.update(bones=['Head']))]
  original=copy.deepcopy((self.r,self.src,self.tail,self.audit))
  for name,fn in changes:
   with self.subTest(gate=name):
    self.r,self.src,self.tail,self.audit=copy.deepcopy(original);(self.d/'candidate.glb').write_bytes(b'candidate');fn();v=self.gate();self.assertFalse(v['numeric_pass']);self.assertFalse(v['checks'][name])
 def test_new_open_cut_fails(self):self.tail['meshes'][0]['boundary_edges_after']=3;self.assertFalse(self.gate()['checks']['closed_cuts'])
 def test_empty_contacts_fail(self):self.r['contact']={};self.r['foot_contact']={};v=self.gate();self.assertFalse(v['checks']['ground']);self.assertFalse(v['checks']['foot_contact'])
 def test_nonfinite_contact_fails(self):self.r['contact']['walk']['lowest_surface_m']=float('nan');self.assertFalse(self.gate()['numeric_pass'])
 def test_incomplete_foot_evidence_fails(self):self.r['foot_contact']['run']['feet']=[];self.assertFalse(self.gate()['numeric_pass'])
 def test_empty_audit_fails(self):self.audit={};self.assertFalse(self.gate()['numeric_pass'])
 def review(self,verdict):
  argv=['forge-realistic-animal','--in',str(self.d/'source.glb'),'--animal','grey wolf','--id','enemy-wolf-realistic-v2','--out',str(self.d),'--review',verdict,'--review-note','Inspected side, back, head and all clip strips.']
  with patch.object(sys,'argv',argv):return cli.main()
 def setup_review(self):
  cli.write(self.d/'verdict.json',self.gate());cli.write(self.d/'stages.json',[]);cli.write(self.d/'run.json',{'id':'enemy-wolf-realistic-v2','animal':'grey wolf','source_sha256':cli.sha(self.d/'source.glb')});cli.write(self.d/'candidate-entry.json',{'id':'enemy-wolf-realistic-v2'})
 def test_bad_never_released(self):self.setup_review();self.assertEqual(self.review('BAD'),0);self.assertFalse((self.d/'catalog_entry.json').exists())
 def test_numeric_failure_not_overridden(self):self.r['triangles']=1;self.setup_review();self.assertEqual(self.review('GOOD'),2);self.assertFalse((self.d/'catalog_entry.json').exists())
 def test_approval_and_immutable_release(self):
  self.setup_review();self.assertEqual(self.review('OK'),0);e=json.loads((self.d/'catalog_entry.json').read_text());self.assertEqual(e['names'],['wolf','wolves']);self.assertEqual(e['style'],'realistic')
  with self.assertRaises(FileExistsError):self.review('OK')
 def test_modified_candidate_not_released(self):
  self.setup_review();(self.d/'candidate.glb').write_bytes(b'changed')
  with self.assertRaises(ValueError):self.review('OK')
 def test_changed_render_blocks_review(self):
  self.setup_review();(self.d/'final-belly.png').write_bytes(b'changed')
  with self.assertRaises(ValueError):self.review('OK')
 def test_cc0_catalog_metadata(self):
  source=self.d/'animal.glb';source.write_bytes(b'ours')
  cli.write(self.d/'catalog_entries.json',[{'file':'animal.glb','licence':'CC0-1.0','credit':'Quaternius, Ultimate Animated Animals'}])
  e=cli.catalog_metadata(source,'wolf','grey wolf',.95)
  self.assertEqual(e['licence'],'CC0-1.0');self.assertIn('Quaternius',e['credit']);self.assertEqual(e['tint'],[]);self.assertIn('forests',e['good_for']);self.assertIn('bites',e['about'])
 def test_unknown_rig_rights_fail_closed(self):
  with self.assertRaises(ValueError):cli.catalog_metadata(self.d/'source.glb','wolf','grey wolf',.95)
 def test_reference_retries_at_most_three(self):
  import types,base64
  (self.d/'neutral.png').write_bytes(b'neutral')
  a=types.SimpleNamespace(ref=None,animal='grey wolf',seed=1)
  response=types.SimpleNamespace(raise_for_status=lambda:None,json=lambda:{'data':[{'b64_json':base64.b64encode(b'picture').decode()}]})
  with patch.object(cli.requests,'post',return_value=response) as request,patch('reference_gate.check',return_value={'pass':False,'checks':{'leg_gaps':False}}):
   with self.assertRaises(ValueError):cli.reference(a,self.d)
   self.assertEqual(request.call_count,3)
   self.assertFalse((self.d/'reference.png').exists())
 def test_no_3d_spend_without_reference_pass(self):
  cli.write(self.d/'reference-gate.json',{'pass':False})
  with patch.object(cli.requests,'post') as request:
   with self.assertRaises(ValueError):cli.generate(None,self.d)
   request.assert_not_called()
 def test_exclusive_copy(self):
  with self.assertRaises(FileExistsError):cli.copy(self.d/'source.glb',self.d/'candidate.glb')
if __name__=='__main__':unittest.main()
