#!/usr/bin/env python3
"""U32 公开入口：四个独立 Gateway 工程的真实 loopback HTTP 运行，记录一次完整成功。

要求本机已有一个可连接的 Redis（限流用例的真实后端）。本入口只使用该实例上的
独有 routeId/用户 key，并在用例内删除与复核；不启动、不重启、不改配置、不扫描全库。
"""
import glob
import hashlib
import json
import os
import pathlib
import platform
import re
import signal
import socket
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
from datetime import datetime

ROOT = pathlib.Path(__file__).resolve().parent
MODULES = [('webflux', 12), ('mvc', 1), ('proxy-mvc', 1), ('proxy-webflux', 1)]


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
    candidates = sorted(glob.glob(os.path.expanduser(
        '~/.m2/wrapper/dists/apache-maven-3.9.9/*/bin/mvn')))
    if candidates:
        return candidates[-1]
    raise SystemExit('需要 Maven 3.9.9：设置 MAVEN_BIN 或在 PATH 中提供 mvn')


def redis_port():
    for arg in sys.argv[1:]:
        if arg.startswith('--redis-port='):
            return int(arg.split('=', 1)[1])
    return int(os.environ.get('U32_REDIS_PORT', '6379'))


def ping(port):
    with socket.create_connection(('127.0.0.1', port), timeout=2) as sock:
        sock.sendall(b'PING\r\n')
        return sock.recv(64).startswith(b'+PONG')


def redis_info(port):
    with socket.create_connection(('127.0.0.1', port), timeout=2) as sock:
        sock.sendall(b'INFO server\r\n')
        payload = b''
        while b'redis_version' not in payload:
            chunk = sock.recv(4096)
            if not chunk:
                break
            payload += chunk
    text = payload.decode('utf-8', 'replace')
    keep = [line for line in text.splitlines() if line.startswith(('redis_version:', 'os:', 'arch_bits:'))]
    return '\n'.join(keep)


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/u32'
logs_root.mkdir(parents=True, exist_ok=True)
logs = pathlib.Path(tempfile.mkdtemp(prefix='run-', dir=logs_root))
started = datetime.now().astimezone().isoformat()
results = []
port = redis_port()
mvn = maven_binary()


def run(name, args, timeout=600, cwd=ROOT):
    process = subprocess.Popen(args, cwd=cwd, env=env, text=True,
                               stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True)
    try:
        output, _ = process.communicate(timeout=timeout)
    except BaseException:
        # 仅本次创建的进程组；Maven fork 的测试 JVM 也在组中，不能只杀父进程留下 listener。
        try:
            os.killpg(process.pid, signal.SIGTERM)
        except ProcessLookupError:
            pass
        try:
            output, _ = process.communicate(timeout=10)
        except subprocess.TimeoutExpired:
            try:
                os.killpg(process.pid, signal.SIGKILL)
            except ProcessLookupError:
                pass
            output, _ = process.communicate(timeout=5)
        (logs / (name + '.log')).write_text(output)
        raise
    (logs / (name + '.log')).write_text(output)
    results.append({'name': name, 'command': args, 'exitCode': process.returncode,
                    'observed': [line for line in output.splitlines() if line.startswith('U32')]})
    if process.returncode:
        raise RuntimeError(name + ' exit=' + str(process.returncode) + '; raw log: '
                           + str(logs / (name + '.log')) + '\n' + output[-3500:])
    print('PASS ' + name, flush=True)
    return output


if not ping(port):
    raise SystemExit('Redis 127.0.0.1:%d 不可连接：限流用例需要真实 Redis，未运行不写成功证据' % port)

version = run('jdk', [str(jdk / 'bin/java'), '-version'])
assert re.search(r'version "21[.]', version), version
run('maven', [mvn, '-version'])
server_info = redis_info(port)
results.append({'name': 'redis', 'command': ['RESP', 'INFO server', '127.0.0.1:%d' % port],
                'exitCode': 0, 'observed': [line for line in server_info.splitlines()]})

reports = []
for module, expected in MODULES:
    args = [mvn, '-B', '-ntp', '-q', '-f', str(ROOT / module / 'pom.xml'), 'clean', 'test']
    if module == 'webflux':
        args.insert(-2, '-Dspring.data.redis.port=%d' % port)
    run('test-' + module, args)
    module_reports = []
    for path in sorted((ROOT / module / 'target/surefire-reports').glob('TEST-*.xml')):
        root = ET.parse(path).getroot()
        module_reports.append({key: root.attrib[key] for key in
                               ['name', 'tests', 'failures', 'errors', 'skipped']})
    total = sum(int(item['tests']) for item in module_reports)
    assert total == expected, (module, total, expected, module_reports)
    reports.extend(module_reports)
test_count = sum(int(item['tests']) for item in reports)

tracked = [ROOT / 'pom.xml', ROOT / 'run.py', ROOT / 'README.md']
tracked += sorted(ROOT.glob('*/pom.xml'))
tracked += sorted(ROOT.glob('*/src/**/*.java'))
tracked += sorted(ROOT.glob('*/src/**/application.properties'))
digests = {}
for path in tracked:
    if path.exists():
        digests[str(path.relative_to(ROOT))] = hashlib.sha256(path.read_bytes()).hexdigest()

def dependency(name):
    tree = run('dependency-' + name, [mvn, '-B', '-ntp', '-q', '-f',
                                     str(ROOT / name / 'pom.xml'), 'dependency:list', '-DoutputFile=/dev/stdout'])
    versions = {}
    for line in tree.splitlines():
        match = re.match(r'\s*[\w.\-]+:([\w.\-]+):(?:jar|pom):([\w.\-]+):', line)
        if match and match.group(1) in {
                'spring-cloud-gateway-server-webflux', 'spring-cloud-gateway-server-webmvc',
                'spring-cloud-gateway-proxyexchange-webmvc', 'spring-cloud-gateway-proxyexchange-webflux',
                'spring-cloud-loadbalancer', 'spring-cloud-commons', 'spring-boot', 'spring-webflux',
                'spring-webmvc', 'spring-security-config', 'reactor-netty-http', 'spring-data-redis'}:
            versions[match.group(1)] = match.group(2)
    return versions


dependencies = dependency('webflux')
evidence = {
    'ticket': 'U32 #69',
    'startedAt': started,
    'finishedAt': datetime.now().astimezone().isoformat(),
    'input': 'python3 labs/cloud-gateway/run.py --redis-port=<本机Redis端口>',
    'exitCode': 0,
    'environment': {
        'jdk': version.strip(),
        'maven': run('maven-report', [mvn, '-version']).strip(),
        'os': platform.platform(),
        'arch': platform.machine(),
        'python': platform.python_version(),
        'redisPort': port,
        'dependencies': dependencies,
    },
    'reports': reports,
    'testCount': test_count,
    'results': results,
    'artifacts': digests,
    'logs': str(logs),
    'cleanup': {
        'redisKeys': 'request_rate_limiter.{<routeId>.<user>}.tokens/timestamp 由用例删除并逐键 EXISTS 复核',
        'processes': '本入口创建的进程组已回收；Redis 为外部实例，只连其端口，不重启不改配置',
    },
    'limits': [
        'loopback HTTP 与受控上游；无真实注册中心、无集群、无生产网关',
        '限流后端为本机 Redis 单实例；未运行集群、故障转移或哨兵',
        'JWT 由本机 RSA 密钥签发，不是 IdP；管理端点鉴权为本地合成 ops 权限',
        '未运行吞吐/容量压测；容量结论为模型与配置推导',
    ],
}
out = ROOT / 'evidence' / ('u32-' + datetime.now().strftime('%Y-%m-%d') + '.json')
if not out.parent.exists():
    out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(evidence, ensure_ascii=False, indent=1) + '\n')
print('WROTE ' + str(out.relative_to(ROOT)) + ' tests=' + str(test_count), flush=True)
