#!/usr/bin/env python3
"""Read-only A02/A03 follow-up audit: actual policy and original tests, exact source guards."""
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
spec=importlib.util.spec_from_file_location('base_audit',Path(__file__).with_name('run_audit.py'))
base=importlib.util.module_from_spec(spec);spec.loader.exec_module(base)
if len(sys.argv)!=2: raise SystemExit('Usage: python3 run_a02_audit.py /absolute/repo/root')
repo=Path(sys.argv[1]).resolve(strict=True); src=repo/base.ROOT_REL
home=(src/'activity/HomeActivity.java').read_text()
sigs=['private void handleA02ScanDuringAlarm(String scanCode, AntiEscapeAlarm alarm)','private void handleA03OriginalTrayRemoved(AntiEscapeAlarm alarm)']
actual=[base.extract_method(home,s) for s in sigs]
claim=base.extract_method(home,'private void claimA02AlarmByTray(final String trayCode, final AntiEscapeAlarm alarm)')
guard=claim[claim.index('{')+1:claim.index('mA02TrayClaimCodePresent = true;')]
java='''import android.text.TextUtils;
import com.cpt.zhct.weighting.utils.Constants;
import com.cpt.zhct.weighting.antiescape.*;
public class A02ActualRouteAudit {
 static class AntiEscapeAlarm { final AlarmType type;AntiEscapeAlarm(AlarmType t){type=t;}AlarmType getType(){return type;}String getSourceTrayCode(){return "003401";}int getPickWeight(){return 10;} }
 static class A02AlarmAction {static final int NONE=0,WAIT_TRAY_CLAIM=1,WAIT_MANAGER_CARD=2;}
 boolean mA03OriginalTrayRemoved=false,mA02TrayClaimCodePresent=true,mA02ClaimRequestInFlight=false,mA02LocalSaveFailed=false;
 String mCurrentCode="003401",mCurrentStaffInfo="old",mA02LastLocalSaveError=""; int scanEmptyFrameCount=0,mTrayRemovalConfirmationCount=3,mA02AlarmAction=A02AlarmAction.WAIT_TRAY_CLAIM,queryCount=0;
 long mFirstEmptyScanElapsedMs=0; final A02TrayClaimCodePolicy mA02TrayClaimCodePolicy=new A02TrayClaimCodePolicy(); final UnlockCodePolicy mUnlockCodePolicy=new UnlockCodePolicy();
 void resolveAntiEscapeAlarm(String x){} void resolveAntiEscapeAlarm(String x,String y){} void switchToBeforeFragment(){}void updateTrayRequiredAlarmAfterTrayRemoval(){} void recordA02Action(AntiEscapeAlarm a,String x,String y){}
 String sanitizeAlarmLog(String x){return x;}String formatAlarmTime(long x){return "0";}long claimPickAtMillis(AntiEscapeAlarm a){return 0;}
 void claimA02AlarmByTray(String trayCode,AntiEscapeAlarm alarm){
'''+guard+'''mA02TrayClaimCodePresent=true;queryCount++;}
'''+ '\n'.join(actual)+'''
 static void eq(Object a,Object b,String why){if(!a.equals(b))throw new AssertionError(why+": "+a+" != "+b);}static void ok(String s){System.out.println("PASS "+s);}
 static A02ActualRouteAudit initialized(){A02ActualRouteAudit f=new A02ActualRouteAudit();f.mA02TrayClaimCodePolicy.setClearConfirmationCount(3);f.mA02TrayClaimCodePolicy.shouldQuery("003401");return f;}
 public static void main(String[] args)throws Exception{
  Object tests=new A02TrayClaimCodePolicyTest();int n=0;for(java.lang.reflect.Method m:tests.getClass().getDeclaredMethods()){if(m.getAnnotation(org.junit.Test.class)!=null){m.invoke(tests);ok("Existing unmodified JUnit test: "+m.getName());n++;}}eq(n,3,"original test count");
  A02TrayClaimCodePolicy p=new A02TrayClaimCodePolicy();p.setClearConfirmationCount(3);p.shouldQuery("003401");p.onTrayCodeCleared();p.onTrayCodeCleared();p.shouldQuery("003401");eq(p.onTrayCodeCleared(),false,"normal valid clears");ok("Actual A02 policy: normal repeated valid code resets prior empty count");
  p=new A02TrayClaimCodePolicy();p.setClearConfirmationCount(3);p.shouldQuery("003401");p.onTrayCodeCleared();p.onTrayCodeCleared();p.setClearConfirmationCount(3);eq(p.onTrayCodeCleared(),true,"same config preserves");ok("Actual A02 policy: setting unchanged N preserves partial empty count");
  p=new A02TrayClaimCodePolicy();p.setClearConfirmationCount(3);p.shouldQuery("003401");p.onTrayCodeCleared();p.onTrayCodeCleared();p.setClearConfirmationCount(4);for(int i=0;i<3;i++)eq(p.onTrayCodeCleared(),false,"changed config clears");eq(p.onTrayCodeCleared(),true,"new threshold");ok("Actual A02 policy: changed N clears old empty count");
  AntiEscapeAlarm a02=new AntiEscapeAlarm(AlarmType.PICK_WITHOUT_TRAY);A02ActualRouteAudit f=initialized();f.mA02ClaimRequestInFlight=true;f.handleA02ScanDuringAlarm("",a02);f.handleA02ScanDuringAlarm("",a02);f.handleA02ScanDuringAlarm("003401",a02);f.handleA02ScanDuringAlarm("",a02);eq(f.mA02TrayClaimCodePresent,false,"inflight guard bypass");ok("Actual A02 route + original query guard: valid code during in-flight query does not break empty count");
  f=initialized();f.handleA02ScanDuringAlarm("",a02);f.handleA02ScanDuringAlarm("",a02);f.handleA02ScanDuringAlarm("888880",a02);f.handleA02ScanDuringAlarm("",a02);eq(f.mA02TrayClaimCodePresent,false,"admin code bypass");ok("Actual A02 route: ignored manager code preserves empty count");
  AntiEscapeAlarm a03=new AntiEscapeAlarm(AlarmType.TRAY_NOT_BOUND);f=initialized();f.handleA02ScanDuringAlarm("",a03);f.handleA02ScanDuringAlarm("",a03);f.handleA02ScanDuringAlarm("003402",a03);eq(f.scanEmptyFrameCount,0,"different code resets A03");eq(f.mA03OriginalTrayRemoved,false,"not yet marked removed");eq(f.queryCount,0,"different code not queried until original removed");ok("Actual A03 route: different nonempty code resets original-tray removal counter, not identity-checked");
  f=initialized();f.mA02AlarmAction=A02AlarmAction.WAIT_MANAGER_CARD;for(int i=0;i<4;i++)f.handleA02ScanDuringAlarm("",a03);eq(f.scanEmptyFrameCount,0,"manager mode skips source removal");eq(f.mA03OriginalTrayRemoved,false,"manager mode no remove");ok("Actual A03 route: waiting for manager card ignores empty input for original-tray removal");
 }
}'''
files={
 'android/text/TextUtils.java':'package android.text;public class TextUtils{public static boolean isEmpty(CharSequence x){return x==null||x.length()==0;}}',
 'org/junit/Test.java':'package org.junit;@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public @interface Test {}',
 'org/junit/Assert.java':'package org.junit;public class Assert{public static void assertTrue(boolean x){if(!x)throw new AssertionError("expected true");}public static void assertFalse(boolean x){if(x)throw new AssertionError("expected false");}}',
 'A02ActualRouteAudit.java':java,
}
rels=['utils/Constants.java','antiescape/AlarmType.java','antiescape/UnlockCodePolicy.java','antiescape/TrayRemovalConfirmationCountPolicy.java','antiescape/A02TrayClaimCodePolicy.java']
source_hashes={}
for rel in rels:
 data=(src/rel).read_bytes();files['com/cpt/zhct/weighting/'+rel]=data.decode();source_hashes[str(base.ROOT_REL/rel)]=base.digest(data)
test_rel=Path('app/src/test/java/com/cpt/zhct/weighting/antiescape/A02TrayClaimCodePolicyTest.java');data=(repo/test_rel).read_bytes();files['com/cpt/zhct/weighting/antiescape/A02TrayClaimCodePolicyTest.java']=data.decode();source_hashes[str(test_rel)]=base.digest(data)
source_hashes[str(base.ROOT_REL/'activity/HomeActivity.java')]=base.digest(home.encode())
javac,java_bin=base.java_tools();status_before=base.run(['git','status','--porcelain'],repo).stdout
with tempfile.TemporaryDirectory(prefix='scan-a02-a03-audit-') as temp:
 work=Path(temp)
 for rel,content in files.items():
  p=work/rel;p.parent.mkdir(parents=True,exist_ok=True);p.write_text(content)
 classes=work/'classes';classes.mkdir()
 base.run([javac,'-encoding','UTF-8','-d',str(classes)]+[str(p) for p in sorted(work.rglob('*.java'))],work)
 execution=base.run([java_bin,'-cp',str(classes),'A02ActualRouteAudit'],work)
results=execution.stdout.strip().splitlines();assert len(results)==10 and all(s.startswith('PASS ') for s in results)
assert status_before==base.run(['git','status','--porcelain'],repo).stdout
print(json.dumps({'head':base.run(['git','rev-parse','HEAD'],repo).stdout.strip(),'source_sha256':source_hashes,'results':results,'result_count':len(results),'boundary':'3 original JUnit test methods run unchanged with minimal Test/Assert compatibility shims, plus 7 actual policy/extracted control-route checks. Not a JUnit platform or Android/network/UI test. Claim method uses the exact leading source guard; subsequent UI/network side effects are replaced by a counter. All business source copies and classes were temporary and removed.'},ensure_ascii=False,indent=2))
