#!/usr/bin/env python3
"""Run U43 on the existing loopback MySQL; record only synthetic observations."""
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import shutil
import subprocess
import tempfile
import uuid
from datetime import datetime

ROOT = Path(__file__).resolve().parent
EVIDENCE = ROOT / 'evidence/u43-2026-10-31.json'
SOURCES = ['pom.xml','README.md','run.py','src/main/java/study/u43/JpaLab.java','src/main/java/study/u43/U43Cleanup.java',
           'src/main/java/study/u43/model/Account.java','src/main/java/study/u43/model/Board.java','src/main/java/study/u43/model/WorkItem.java','src/main/java/study/u43/model/Department.java','src/main/java/study/u43/model/Employee.java',
           'src/main/java/study/u43/repo/AccountRepository.java','src/main/java/study/u43/repo/AccountView.java','src/main/java/study/u43/repo/EmployeeRepository.java']

def diagnostics(output):
    if isinstance(output, bytes): output = output.decode('utf-8', 'replace')
    output = output or ''
    for secret in (os.environ.get('U43_MYSQL_PASSWORD', ''), os.environ.get('U43_MYSQL_USER', '')):
        if secret: output = output.replace(secret, '<REDACTED>')
    lines = [line for line in output.splitlines() if any(x in line for x in ('Exception', 'ERROR', 'Caused by:', 'error', '拒绝'))]
    return [re.sub(r'(?i)(password|pwd)=([^&\s]+)', r'\1=<REDACTED>', line)[:500] for line in lines[-6:]]

def main():
    java_home = Path(os.environ.get('JAVA_HOME', '/Library/Java/JavaVirtualMachines/liberica-jdk-21.jdk/Contents/Home'))
    java = java_home / 'bin/java'
    mvn = shutil.which('mvn')
    if not java.is_file() or not mvn:
        raise RuntimeError('需要 JDK 21 和 Maven 3.9，且 MySQL 由 DBngin 运行于 127.0.0.1:3306')
    allowed = ('PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_ALL', 'TZ', 'U43_MYSQL_USER', 'U43_MYSQL_PASSWORD', 'U43_TEST_ABORT_AFTER_CREATE')
    probe = dict(os.environ, SPRING_JPA_PROPERTIES_HIBERNATE_DEFAULT_CATALOG='u43_must_never_access', JAVA_TOOL_OPTIONS='-Dspring.jpa.properties.hibernate.default_catalog=u43_must_never_access')
    env = {key: probe[key] for key in allowed if key in probe}
    if 'SPRING_JPA_PROPERTIES_HIBERNATE_DEFAULT_CATALOG' in env or 'JAVA_TOOL_OPTIONS' in env: raise RuntimeError('环境隔离失败')
    env.update(JAVA_HOME=str(java_home), MAVEN_OPTS='-Xmx384m -XX:ActiveProcessorCount=2')
    java_version = subprocess.check_output([str(java), '-version'], stderr=subprocess.STDOUT, text=True, env=env).strip()
    if '21.0.11' not in java_version: raise RuntimeError('实验基线需要 JDK 21.0.11')
    result = {'issue': 80, 'input': 'python3 labs/jpa-u43/run.py', 'startedAt': datetime.now().astimezone().isoformat(),
              'environment': {'os': platform.platform(), 'arch': platform.machine()}, 'status': 'failed', 'exitCode': 1,
              'observations': []}
    schema = 'study_u43_' + uuid.uuid4().hex[:12]
    result['schema'] = schema
    result['environmentIsolation'] = '合成的 SPRING_JPA_PROPERTIES_HIBERNATE_DEFAULT_CATALOG 和 JAVA_TOOL_OPTIONS 在子进程环境中均不存在；实验核验 catalog 配置不存在。'
    try:
        with tempfile.TemporaryDirectory(prefix='jpa-u43-') as directory:
            cp = Path(directory) / 'classpath.txt'
            cmd = [mvn, '-o', '-B', '-q', 'compile', 'org.apache.maven.plugins:maven-dependency-plugin:3.8.1:build-classpath', '-DincludeScope=runtime', f'-Dmdep.outputFile={cp}']
            subprocess.run(cmd, cwd=ROOT, env=env, check=True, timeout=180, capture_output=True, text=True)
            classpath = str(ROOT / 'target/classes') + os.pathsep + cp.read_text().strip()
            versions = [x for x in cp.read_text().split(os.pathsep) if any(k in x for k in ['/hibernate-core/', '/spring-data-jpa/', '/jakarta.persistence-api/', '/mysql-connector-j/'])]
            result['dependencies'] = [Path(x).name for x in versions]
            for required in ['hibernate-core-7.2.25.Final.jar', 'spring-data-jpa-4.0.8.jar', 'jakarta.persistence-api-3.2.0.jar', 'mysql-connector-j-9.7.0.jar']:
                if required not in result['dependencies']: raise RuntimeError('依赖版本不符: ' + required)
            # The child receives a whitelist, excluding Spring/JVM option injection.
            command = [str(java), '-Dspring.config.location=optional:file:/dev/null', '-Xms64m', '-Xmx256m', '-XX:ActiveProcessorCount=2', '-cp', classpath, 'study.u43.JpaLab', schema]
            returncode = None
            result['phase'] = 'java-lab'
            stderr = ''
            try:
                run = subprocess.run(command, cwd=ROOT, env=env, capture_output=True, text=True, timeout=120)
                stdout, stderr, returncode = run.stdout, run.stderr, run.returncode
            except subprocess.TimeoutExpired as exc:
                stdout = (exc.stdout or b'').decode('utf-8', 'replace') if isinstance(exc.stdout, bytes) else (exc.stdout or '')
                stderr = exc.stderr or '' 
            result['observations'] = [line for line in stdout.splitlines() if line.startswith('U43 ')]
            created = 'U43 schema-created=' + schema in result['observations']
            cleaned = 'U43 cleanup=PASS own-schema=removed' in result['observations']
            if created and not cleaned:
                rescue = subprocess.run([str(java), '-Xmx64m', '-cp', classpath, 'study.u43.U43Cleanup', schema], cwd=ROOT, env=env, capture_output=True, text=True, timeout=30)
                if rescue.returncode != 0:
                    result['diagnostics'] = diagnostics(rescue.stderr + rescue.stdout)
                    raise RuntimeError('本次 schema 应急清理失败，请检查 ' + schema)
                result['observations'].append('U43 emergency-cleanup=PASS own-schema=removed')
            markers = ['schema-created=', 'mysql=', 'target=PASS', 'lifecycle=PASS', 'mapping=PASS', 'repository=PASS', 'fetching=PASS', 'optimistic=PASS', 'batch=PASS', 'pessimistic=PASS', 'cleanup=PASS']
            if returncode != 0 or 'U43 mysql=8.4.7' not in result['observations'] or not all(any(line.startswith('U43 ' + m) for line in result['observations']) for m in markers):
                result['diagnostics'] = diagnostics(stderr) + diagnostics(stdout)
                raise RuntimeError('实验进程失败或观察缺失；exit=' + str(returncode) + ('；本次 schema 已清理' if cleaned or created else '；未观察到创建标记，请核对 schema ' + schema))
            result.update(status='passed', exitCode=0, java=java_version,
                limits='MySQL 8.4.7 单机；未验证跨主机网络故障、生产容量、集群二级缓存或所有方言。',
                artifactSha256={p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() for p in SOURCES})
    except Exception as exc:
        if isinstance(exc, subprocess.CalledProcessError):
            result['phase'] = 'maven'
            result['diagnostics'] = diagnostics(exc.stderr) + diagnostics(exc.stdout)
        result['error'] = type(exc).__name__ + ': ' + str(exc)
        raise
    finally:
        if result['status'] == 'passed':
            EVIDENCE.parent.mkdir(parents=True, exist_ok=True)
            tmp = EVIDENCE.with_suffix('.tmp')
            tmp.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n')
            tmp.replace(EVIDENCE)
        else:
            print(json.dumps(result, ensure_ascii=False, indent=2))
    print(json.dumps(result, ensure_ascii=False, indent=2))

if __name__ == '__main__':
    main()
