#!/usr/bin/env python3
"""Meaningful synthetic-method and immutable-result checks; no human outcomes."""
import copy,hashlib,importlib.util,json,math,pathlib,sys,tempfile,unittest,xml.etree.ElementTree as E
sys.dont_write_bytecode=True
HERE=pathlib.Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('a08tools',HERE/'research_tools.py');rt=importlib.util.module_from_spec(spec);spec.loader.exec_module(rt)

def xml(permissions,doi='10.test/source'):
 return E.fromstring('<article article-type="research-article"><front><journal-meta><journal-title-group><journal-title>Fictional fixture journal</journal-title></journal-title-group></journal-meta><article-meta><article-id pub-id-type="doi">'+doi+'</article-id><title-group><article-title>Wholly fictional test source</article-title></title-group><contrib-group><contrib contrib-type="author"><name><given-names>A</given-names><surname>Researcher</surname></name></contrib><contrib contrib-type="editor"><name><given-names>B</given-names><surname>Editor</surname></name></contrib></contrib-group><permissions>'+permissions+'</permissions></article-meta></front><body><sec><p>Entirely synthetic fixture; not research observations.</p></sec></body></article>')

def fixture():
 return [dict(participant_id=f'{s}-{a}-{i}',stratum=str(s),arm=a,mean_score=4+2*i+2*a,caseCount=12) for s in range(4) for a in [0,1] for i in range(2)]

class Methods(unittest.TestCase):
 def test_hand_calculated_equal_cells(self):
  v=rt.blocked_estimate(fixture());self.assertAlmostEqual(v['effect_points'],2);self.assertAlmostEqual(v['standard_error'],math.sqrt(.5));self.assertAlmostEqual(v['df'],8);self.assertLess(v['ci95'][0],2);self.assertGreater(v['ci95'][1],2)
 def test_heteroskedastic_welch_df(self):
  rows=fixture()
  for r in rows:
   if r['stratum']=='0' and r['arm']==1:r['mean_score']=3 if r['participant_id'].endswith('-0') else 15
  v=rt.blocked_estimate(rows)
  terms=[72/(16*2)]+[2/(16*2)]*7
  self.assertAlmostEqual(v['df'],sum(terms)**2/sum(x*x for x in terms));self.assertLess(v['df'],8)
 def test_duplicate_missing_and_invalid_rejected(self):
  rows=fixture()
  with self.assertRaises(ValueError):rt.blocked_estimate(rows+[rows[0]])
  with self.assertRaises(ValueError):rt.blocked_estimate([r for r in rows if r['stratum']!='0'])
  for value in [None,float('nan'),float('inf'),25,-1]:
   x=copy.deepcopy(rows);x[0]['mean_score']=value
   with self.assertRaises((ValueError,TypeError)):rt.blocked_estimate(x)
  x=copy.deepcopy(rows);x[0]['arm']=7
  with self.assertRaises(ValueError):rt.blocked_estimate(x)
 def test_rights_ALI_and_data_CC0(self):
  a=xml('<license><ali:license_ref xmlns:ali="http://www.niso.org/schemas/ali/1.0/">https://creativecommons.org/licenses/by/4.0/</ali:license_ref></license>');self.assertEqual(rt.rights(a)['status'],'eligible-article-license')
  b=xml('<license><license-p>The data made available have a CC0waiver.<ext-link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://creativecommons.org/publicdomain/zero/1.0/"/></license-p></license>');self.assertEqual(rt.rights(b)['status'],'unresolved')
  c=xml('<license><license-p>Article licensed CC BY; data waiver CC0.<ext-link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://creativecommons.org/licenses/by/4.0/"/><ext-link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://creativecommons.org/publicdomain/zero/1.0/"/></license-p></license>');self.assertEqual(rt.rights(c)['licenseName'],'CC BY 4.0')
  d=xml('<license><license-p>No commercial re-use.<ext-link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://creativecommons.org/licenses/by/4.0/"/></license-p></license>');self.assertEqual(rt.rights(d)['status'],'restricted-or-conflicting')
  e=xml('<license><license-p>CC BY</license-p></license>');self.assertEqual(rt.rights(e)['status'],'unresolved')
 def test_authors_and_exact_doi_status(self):
  root=xml('<license><ext-link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://creativecommons.org/licenses/by/4.0/"/></license>')
  rec={'pmcid':'PMC-FICTIONAL','doi':'10.test/other','title':'Fictional fixture','isRetracted':'Y'}
  r=rt.audit_record(rec,E.tostring(root));self.assertEqual(r['authors'],['A Researcher']);self.assertIn('doi-conflict',r['exclusionReasons']);self.assertIn('status-signal-needs-review',r['exclusionReasons']);self.assertEqual(r['candidateStatus'],'excluded-or-unresolved')
 def test_editor_role_without_contributor_type(self):
  root=xml('');group=root.find('./front/article-meta/contrib-group')
  group.append(E.fromstring('<contrib><name><given-names>C</given-names><surname>AcademicEditor</surname></name><role>Academic Editor</role></contrib>'))
  group.append(E.fromstring('<contrib><name><given-names>D</given-names><surname>Writer</surname></name><role>Writing – review &amp; editing</role></contrib>'))
  self.assertEqual(rt.authors(root),['A Researcher','D Writer'])
 def test_frozen_audit_and_results(self):
  c=json.loads((HERE/'corpus-audit.json').read_text());self.assertEqual(c['auditedRecords'],36);self.assertEqual(c['databaseHitCount'],98);self.assertEqual(c['counts']['source-eligible-for-benchmark-construction'],23);self.assertEqual(c['counts']['excluded-or-unresolved'],13);self.assertEqual(c['licenseCounts'],{'eligible-article-license':29,'restricted-or-conflicting':4,'unresolved':3});self.assertEqual(c['deduplication']['duplicateDoiCount'],0)
  rows={r['pmcid']:r for r in c['records']};self.assertEqual(rows['PMC13560027']['sourceVersionDois'],['10.12688/f1000research.183874.3']);self.assertEqual(rows['PMC13560027']['doi'],'10.12688/f1000research.183874.2');self.assertIn('doi-conflict',rows['PMC13560027']['exclusionReasons'])
  self.assertEqual(rows['PMC12676084']['jatsPublicationDates'][0]['date'],{'year':'2025','month':'12','day':'3'})
  for r in rows.values():self.assertEqual(len(r['sourceTextSha256']),64);self.assertGreater(r['bodyParagraphs'],0)
  d=json.loads((HERE/'design-analysis.json').read_text());self.assertEqual(d['actualHumanParticipants'],0);self.assertEqual(len(d['scenarios']),9);self.assertEqual(d['replicatesPerScenario'],10000)
  for r in d['scenarios']:
   self.assertTrue(.03<r['nullRejectionRate']<.07);self.assertAlmostEqual(r['powerMonteCarloSE'],math.sqrt(r['power']*(1-r['power'])/10000))
 def test_seed_reproducibility(self):
  with tempfile.TemporaryDirectory() as d:
   a=rt.simulate(pathlib.Path(d)/'a.json',reps=200,seed=7);b=rt.simulate(pathlib.Path(d)/'b.json',reps=200,seed=7);self.assertEqual(a,b)
  with self.assertRaises(ValueError):rt.simulate('/tmp/unused-a08.json',reps=1)
 def test_source_verification_limits(self):
  s=json.loads((HERE/'source-audit.json').read_text());r={x['referenceNumber']:x for x in s['references']};self.assertFalse(r[15]['fullTextAccessible']);self.assertTrue(r[14]['fullTextAccessible']);self.assertEqual(len(r),16)
  spirit=[x for x in s['supplementalSourceRecords'] if x['pmcid']=='PMC12035670'][0];self.assertEqual(spirit['license']['status'],'restricted-or-conflicting')

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