#!/usr/bin/env python3
"""U49 公开入口：OXM（JAXB/Jackson XML）、JMX（平台 MBeanServer）、邮件（GreenMail）与 DataBuffer。

全部测试在本机进程内执行，不依赖外部服务：JMX 使用 JVM 平台 MBeanServer；
GreenMail 嵌入式 SMTP/IMAP 只监听 loopback 自动分配端口；数据缓冲实验使用真实
UnpooledByteBufAllocator。入口运行 `clean test`，核对 Surefire 计数、14 条观察行
与依赖版本，写入 evidence JSON。
"""
import glob
import hashlib
import json
import os
import pathlib
import platform
import re
import signal
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
from datetime import datetime

ROOT = pathlib.Path(__file__).resolve().parent
EXPECTED = {
    'study.springcaps.JaxbOxmTest': 4,
    'study.springcaps.JmxPlatformTest': 3,
    'study.springcaps.DataBufferLifecycleTest': 4,
    'study.springcaps.MailGreenMailTest': 3,
}
MARKERS = [
    'U49 oxmRoundTrip=',
    'U49 oxDtdReject=',
    'U49 oxmBadXml=',
    'U49 oxmDtdToggle=',
    'U49 oxmSchemaValidation=',
    'U49 jacksonXmlContrast=',
    'U49 jmxRegister=',
    'U49 jmxLifecycle=',
    'U49 jmxConflictFail=',
    'U49 jmxBootEndpoint=',
    'U49 dbRefCount=',
    'U49 dbDoubleRelease=',
    'U49 dbDefaultNotPooled=',
    'U49 dbJoinLimit=',
    'U49 dbLeak=',
    'U49 mailSend=',
    'U49 mailAuthFail=',
    'U49 mailRefused=',
    'U49 mailPartial=',
]
VERSION_BOUNDS = {
    'org.springframework:spring-oxm': '7.0.10',
    'org.springframework:spring-context-support': '7.0.10',
    'org.springframework.boot:spring-boot-mail': '4.0.9',
    'org.springframework.boot:spring-boot-actuator-autoconfigure': '4.0.9',
    'jakarta.mail:jakarta.mail-api': '2.1.5',
    'org.eclipse.angus:angus-mail': '2.0.5',
    'com.fasterxml.jackson.dataformat:jackson-dataformat-xml': '2.21.7',
    'org.glassfish.jaxb:jaxb-runtime': '4.0.9',
    'com.icegreen:greenmail': '2.1.9',
    'io.netty:netty-buffer': '4.2.19.Final',
}


def maven_binary():
    found = os.environ.get('MAVEN_BIN') or subprocess.run(
        ['/usr/bin/which', 'mvn'], capture_output=True, text=True).stdout.strip()
    if found:
        return found
    raise SystemExit('需要 Maven 3.9.9：设置 MAVEN_BIN 或在 PATH 中提供 mvn')


def interrupted(signum, _frame):
    raise SystemExit(128 + signum)


signal.signal(signal.SIGTERM, interrupted)
signal.signal(signal.SIGINT, interrupted)

jdk = 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(jdk), PATH=str(jdk / 'bin') + os.pathsep + os.environ.get('PATH', ''))
logs_root = pathlib.Path.home() / '.cache/software-engineering-study/java-depth-impl/u49'
logs_root.mkdir(parents=True, exist_ok=True)
logs = pathlib.Path(tempfile.mkdtemp(prefix='run-', dir=logs_root))
started = datetime.now().astimezone().isoformat()
results = []
mvn = maven_binary()


def run(name, args, timeout=900, cwd=ROOT):
    log_path = logs / (name + '.log')
    process = None
    try:
        with log_path.open('w') as stream:
            process = subprocess.Popen(args, cwd=cwd, env=env, text=True,
                                       stdout=stream, stderr=subprocess.STDOUT,
                                       start_new_session=True)
            return_code = process.wait(timeout=timeout)
            exit_code = return_code if return_code >= 0 else 128 - return_code
    except subprocess.TimeoutExpired:
        exit_code = 124
        if process is not None:
            os.killpg(process.pid, signal.SIGKILL)
    output = log_path.read_text(errors='replace') if log_path.exists() else ''
    result = {'name': name, 'command': args, 'exitCode': exit_code,
              'observed': [line for line in output.splitlines() if line.startswith('U49')]}
    results.append(result)
    if exit_code:
        print(name + ' exit=' + str(exit_code) + '; raw log: ' + str(log_path)
              + '\n' + output[-3000:], file=sys.stderr, flush=True)
        raise SystemExit(exit_code)
    print('PASS ' + name, flush=True)
    return output


def surefire_reports():
    reports = []
    for path in sorted((ROOT / 'target/surefire-reports').glob('TEST-*.xml')):
        root = ET.parse(path).getroot()
        reports.append({'name': root.get('name'), 'tests': root.get('tests'),
                        'failures': root.get('failures'), 'errors': root.get('errors'),
                        'skipped': root.get('skipped'), 'time': root.get('time')})
    return reports


def resolved_versions():
    versions = {}
    tree = subprocess.run([mvn, '-B', 'dependency:tree'], cwd=ROOT, env=env,
                          capture_output=True, text=True, timeout=600).stdout
    for line in tree.splitlines():
        field = re.search(r'[(\\+|)-]*\s*([\w.]+):([\w.\-]+):jar:([\w.\-]+):(compile|runtime|test)', line)
        if field:
            key = field.group(2)
            if key in {k.split(':')[1] for k in VERSION_BOUNDS}:
                coord = None
                for bound in VERSION_BOUNDS:
                    if bound.split(':')[1] == key:
                        coord = bound
                        break
                if coord and coord not in versions:
                    versions[coord] = field.group(3)
    return versions


def file_digests():
    digests = {}
    for path in sorted(ROOT.rglob('*')):
        if not path.is_file():
            continue
        parts = path.relative_to(ROOT).parts
        if 'target' in parts or 'evidence' in parts:
            continue
        digests[str(path.relative_to(ROOT))] = hashlib.sha256(path.read_bytes()).hexdigest()
    return digests


def main():
    run('clean-test', [mvn, '-B', '-q', 'clean', 'test'])

    reports = surefire_reports()
    total = 0
    for report in reports:
        expected = EXPECTED.get(report['name'])
        if expected is None:
            raise SystemExit('未登记的报告: ' + report['name'])
        if report['tests'] != str(expected):
            raise SystemExit(f"{report['name']}: 期望 {expected}，实际 {report['tests']}")
        for key in ('failures', 'errors', 'skipped'):
            if report[key] != '0':
                raise SystemExit(f"{report['name']}: {key}={report[key]}")
        total += expected
    if set(EXPECTED) != {r['name'] for r in reports}:
        raise SystemExit('报告集合不一致')

    observed = [line for r in results for line in r['observed']]
    if len(observed) != len(MARKERS):
        raise SystemExit('观察行数应为 ' + str(len(MARKERS)) + '，实际 ' + str(len(observed)))
    for marker in MARKERS:
        hits = [line for line in observed if line.startswith(marker)]
        if len(hits) != 1:
            raise SystemExit('标记应恰好出现一次: ' + marker + '，实际 ' + str(len(hits)))

    versions = resolved_versions()
    for coord, bound in VERSION_BOUNDS.items():
        actual = versions.get(coord)
        if actual != bound:
            raise SystemExit(f'{coord}: 期望 {bound}，实际 {actual}')

    evidence = {
        'ticket': 'U49 #86',
        'generatedAt': datetime.now().astimezone().isoformat(),
        'startedAt': started,
        'purpose': 'OXM（JAXB/Jackson XML）、JMX（平台 MBeanServer）、邮件（GreenMail SMTP/IMAP）与 DataBuffer 引用计数/上限的真实运行',
        'exitCode': 0,
        'testCount': total,
        'reports': reports,
        'results': results,
        'observed': observed,
        'environment': {
            'os': platform.platform(),
            'arch': platform.machine(),
            'javaVersion': subprocess.check_output([str(jdk / 'bin/java'), '-version'],
                                                   stderr=subprocess.STDOUT, text=True).splitlines()[0],
            'maven': subprocess.check_output([mvn, '-v'], text=True).splitlines()[0],
            'versions': versions,
            'versionsSource': 'Spring Boot 4.0.9 parent/BOM（greenmail 2.1.9 显式钉版）',
            'sourceTag': 'spring-projects/spring-framework v7.0.10（Jaxb2Marshaller/JavaMailSenderImpl/MBeanExporter/DataBufferUtils 固定源码）',
        },
        'files': file_digests(),
        'limits': [
            'JMX 只在 JVM 平台 MBeanServer 上注册与调用；远程连接器（jmxmp/RMI service:jmx:remote）未开放也未运行。',
            'GreenMail 是嵌入式真实 SMTP/IMAP 服务器（真实 socket 协议交互），不是生产级 MTA；投递策略、中继与 TLS 未运行。',
            '邮件部分失败场景用服务端停止造成 transport 断开验证失败映射；RCPT 级部分拒绝（GreenMail 不做 RCPT 校验）未运行。',
            'DataBuffer 泄漏观察依赖 ResourceLeakDetector PARANOID 的运行时日志，未开启 fail-on-leak 断言。',
            'Jackson XML 只做对照（默认行为差异），生产 MIME 配置（JacksonXmlHttpMessageConverter 装配）未运行。',
            'Spring 官方 oxm/jmx/email 文档中的 XStream/JiBX 等其他 Marshaller、XML config namespace 装配未运行。',
        ],
    }
    EVIDENCE = ROOT / 'evidence'
    EVIDENCE.mkdir(exist_ok=True)
    target = EVIDENCE / 'u49-2026-10-29.json'
    target.write_text(json.dumps(evidence, ensure_ascii=False, indent=1) + '\n')
    print('U49 PASS tests=' + str(total) + ' evidence=' + str(target.relative_to(ROOT)), flush=True)
    return 0


if __name__ == '__main__':
    sys.exit(main())
