#!/usr/bin/env python3
"""U25: TestContext cache/isolation, real-rollback transactions, MockMvc vs real port, Boot4 slices, client error paths."""
import hashlib,json,os,pathlib,platform,re,subprocess
from datetime import datetime
ROOT=pathlib.Path(__file__).resolve().parent
JAVA_HOME=pathlib.Path(os.environ.get('JAVA_HOME') or subprocess.check_output(['/usr/libexec/java_home','-v','21'],text=True).strip())
ENV=dict(os.environ,JAVA_HOME=str(JAVA_HOME),PATH=str(JAVA_HOME/'bin')+os.pathsep+os.environ.get('PATH',''))
VERSION=subprocess.check_output([str(JAVA_HOME/'bin/java'),'-version'],stderr=subprocess.STDOUT,text=True,env=ENV)
assert re.search(r'version "21[.]',VERSION),VERSION
LOGS=pathlib.Path.home()/'.cache/software-engineering-study/java-depth-impl/u25/runtime';LOGS.mkdir(parents=True,exist_ok=True)
RESULTS=[]
INVOCATION=datetime.now().astimezone().isoformat()

def command(name,args,markers=(),expected=0):
    started=datetime.now().astimezone().isoformat()
    p=subprocess.run([str(a) for a in args],cwd=ROOT,env=ENV,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=600)
    (LOGS/(name+'.log')).write_text(p.stdout)
    assert p.returncode==expected,(name,p.returncode,p.stdout[-7000:])
    for marker in markers:assert marker in p.stdout,(name,marker,p.stdout[-7000:])
    entry={'name':name,'invocationId':INVOCATION,'startedAt':started,'finishedAt':datetime.now().astimezone().isoformat(),'exitCode':p.returncode,'input':[str(a) for a in args]}
    observed=[line for line in p.stdout.splitlines() if line.startswith('U25OBSERVE')]
    if observed:entry['observed']=observed
    RESULTS.append(entry)
    print('PASS',name,flush=True)
    return p.stdout

def positive_suite():
    out=command('all-modules-test',['mvn','-q','test'],markers=('U25OBSERVE context ContextRefreshedEvent',))
    assert 'U25OBSERVE context ContextClosedEvent' in out
    assert 'U25OBSERVE tx rollbackGroup absentAfterRollback=0' in out
    assert 'U25OBSERVE tx commitGroup visibleFromFreshServerConnection=1' in out
    assert 'U25OBSERVE mockmvc-filters open=200 anonymous=401' in out
    assert 'U25OBSERVE mockmvc-nofilters anonymousHello=200' in out
    assert 'U25OBSERVE mocked-users mockPassword=200 productionPassword=401' in out
    assert 'U25OBSERVE standalone anonymousHello=200 anonymousAdmin=200' in out
    assert 'U25OBSERVE real-port' in out
    assert 'U25OBSERVE webmvc-slice serviceBeans=mock jdbcBeans=0' in out
    assert 'U25OBSERVE webmvc-slice securityFilterChains=0 anonymous=200' in out
    assert 'U25OBSERVE webmvc-slice-security securityFilterChains=1 anonymous=401' in out
    assert 'U25OBSERVE jdbc-slice' in out and 'U25OBSERVE redis-slice' in out
    assert 'U25OBSERVE client successPath=200' in out and 'U25OBSERVE client serverError=500' in out
    assert 'U25OBSERVE client readTimeout=ResourceAccessException' in out and 'U25OBSERVE client connectionRefused=ResourceAccessException' in out
    assert 'U25OBSERVE summary refreshCount=4 closeCount=1' in out

def negative_missing_service_probe():
    # 不给 @MockitoBean 补位：@WebMvcTest 切片加载必须失败（预期退出码 1）。
    command('webmvc-slice-missing-service',['mvn','-q','-pl','slices','test','-Dtest=WebMvcSliceMissingServiceProbe'],expected=1)
    report=ROOT/'slices/target/surefire-reports/study.testing.slices.WebMvcSliceMissingServiceProbe.txt'
    text=report.read_text()
    assert 'UnsatisfiedDependencyException' in text or 'NoSuchBeanDefinitionException' in text,text[-4000:]
    assert 'OrderService' in text,text[-4000:]
    RESULTS.append({'name':'webmvc-slice-missing-service-observed','invocationId':INVOCATION,
        'failure':re.search(r'(UnsatisfiedDependencyException|NoSuchBeanDefinitionException)[^\n]*',text).group(0),
        'observed':['U25OBSERVE negative slice missing OrderService fails context load']})
    print('PASS webmvc-slice-missing-service-observed',flush=True)

def write_evidence():
    tree=command('web-dependency-tree',['mvn','-pl','web','dependency:tree','-Dincludes=org.springframework.boot:spring-boot-starter-parent,org.springframework:spring-test,org.springframework.boot:spring-boot-test,org.springframework.boot:spring-boot-webmvc-test,org.springframework.security:spring-security-config'],markers=('spring-test:jar:7.0.10',))
    def version(artifact):
        match=re.search(re.escape(artifact)+r':jar:([0-9][^:]+)',tree);assert match,(artifact,'version not resolved')
        return match.group(1)
    sources=b''.join(sorted(p.read_bytes() for p in ROOT.rglob('*') if p.suffix in ('.java','.xml','.properties') and 'target' not in p.parts))
    evidence={'ticket':'U25 #62','generatedAt':datetime.now().astimezone().isoformat(),
      'environment':{'os':platform.platform(),'arch':platform.machine(),'jdk':VERSION.strip(),'boot':'4.0.9','framework':version('org.springframework:spring-test'),'security':version('org.springframework.security:spring-security-config'),'mysql':'127.0.0.1:3306 root empty password, synthetic database u25_<uuid> per run','redis':'127.0.0.1:6379 database 13, own key prefix u25:<uuid>'},
      'input':'JAVA_HOME=$(/usr/libexec/java_home -v21) python3 labs/boot-testing/run.py',
      'latestInvocation':{'id':INVOCATION,'finishedAt':datetime.now().astimezone().isoformat()},
      'artifactSha256':hashlib.sha256(sources).hexdigest(),
      'runnerSha256':hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest(),
      'exitCode':0,'rawLogs':str(LOGS),'results':RESULTS,
      'limits':['Loopback tests only: MockMvc cases never start a servlet container, TLS or real network dispatch; real-container evidence is limited to the RANDOM_PORT Tomcat cases in the web module',
        'Transaction proofs run against the local MySQL instance with a per-run synthetic database u25_<uuid> that is dropped afterwards; no other database or user data is touched',
        'Redis slice uses database 13 with own u25:<uuid> key prefix and deletes only its own keys; Redis server configuration is untouched',
        'The negative slice probe intentionally fails context load; it is listed as a separate result with exit code 1 and is not part of the passing aggregate run',
        'Client error paths use a JDK built-in HttpServer fixture on loopback; no external service, proxy or production endpoint is contacted']}
    (ROOT/'evidence').mkdir(exist_ok=True)
    (ROOT/'evidence/u25-2026-10-26.json').write_text(json.dumps(evidence,ensure_ascii=False,indent=2)+'\n')
    print('PASS U25, evidence written',flush=True)

if __name__=='__main__':
    positive_suite()
    negative_missing_service_probe()
    write_evidence()
