#!/usr/bin/env python3
"""U62 公开入口：Maven 多模块/依赖调解/BOM/插件门禁/可重复构建 + Gradle 最小对照 + Git 协作冲突。

三类输入全部是本机真实工具链：Maven 3.9.9、JDK 21、Gradle 9.8.2（缓存在
~/.cache/software-engineering-study/tools，不入仓库）、git。所有临时仓库都在本脚本
创建的目录里，结束时删除；不改本仓库的 Git 状态。
"""
import hashlib
import json
import os
import pathlib
import platform
import re
import shutil
import signal
import subprocess
import sys
import tempfile
from datetime import datetime

ROOT = pathlib.Path(__file__).resolve().parent
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', ''))
GRADLE_HOME = pathlib.Path.home() / '.cache/software-engineering-study/gradle-home'
GRADLE_BIN = pathlib.Path.home() / '.cache/software-engineering-study/tools/gradle-9.8.2/bin/gradle'
LOGS = pathlib.Path.home() / '.cache/software-engineering-study/java-depth-impl/u62'
LOGS.mkdir(parents=True, exist_ok=True)
RUN = pathlib.Path(tempfile.mkdtemp(prefix='run-', dir=LOGS))
MWD = RUN / 'maven'
GWD = RUN / 'gradle'
GITD = RUN / 'git'
results = []
OBSERVED = []
started = datetime.now().astimezone().isoformat()


def mvn_binary():
    found = shutil.which('mvn')
    if found:
        return found
    raise SystemExit('需要 Maven 3.9.x')


MVN = mvn_binary()


def mark(line):
    OBSERVED.append(line)
    print(line, flush=True)


def run(name, args, cwd, timeout=600, expect=0, env=None):
    log = RUN / (name + '.log')
    try:
        proc = subprocess.run(args, cwd=cwd, env=env or ENV, text=True,
                              capture_output=True, timeout=timeout)
    except subprocess.TimeoutExpired:
        raise SystemExit(f'{name} 超时')
    combined = (proc.stdout or '') + (proc.stderr or '')
    log.write_text(combined)
    ok = proc.returncode == expect
    results.append({'name': name, 'command': args, 'cwd': str(cwd),
                    'exitCode': proc.returncode, 'expected': expect, 'ok': ok,
                    'observed': [l for l in (proc.stdout or '').splitlines() if l.startswith('U62')]})
    print(('PASS ' if ok else 'FAIL ') + name + f' exit={proc.returncode} expected={expect}', flush=True)
    if not ok:
        print((proc.stdout or '')[-2500:] + (proc.stderr or '')[-2500:], file=sys.stderr)
        raise SystemExit(1)
    return combined

# ---------- 0. 把仓库里的构建工程复制到临时目录（不污染源码树 target/） ----------
# 注意：Java 包路径 study/build/... 里也有 build 目录，Maven 副本不能按名字忽略 build
shutil.copytree(ROOT, MWD, ignore=shutil.ignore_patterns('target', '.gradle', 'gradle-project', 'evidence'))
shutil.copytree(ROOT / 'gradle-project', GWD, ignore=shutil.ignore_patterns('target', 'build', '.gradle'))

# ---------- 1. 多模块构建顺序与 BOM 管理 ----------
out = run('maven-version', [MVN, '-B', '-ntp', '-v'], ROOT)
run('maven-install', [MVN, '-B', '-ntp', '-q', 'install', '-DskipTests'], MWD)
tree = run('maven-tree-managed', [MVN, '-B', '-ntp', 'dependency:tree', '-pl', 'app', '-Dincludes=com.fasterxml.jackson.core'], MWD)
managed = re.search(r'jackson-databind:jar:([\w.]+):compile', tree)
mark('U62 mavenManagedBom=PASS jackson=' + (managed.group(1) if managed else 'missing'))
assert managed and managed.group(1) == '2.21.7', 'BOM 管理的版本应为 2.21.7'

# ---------- 2. 无 BOM 对照：版本由调解顺序决定，而不是固定版本 ----------
pom_backup = (MWD / 'pom.xml').read_text()
lib_backup = (MWD / 'lib/pom.xml').read_text()
app_backup = (MWD / 'app/pom.xml').read_text()
no_bom = pom_backup.replace(""" <dependencyManagement>
  <dependencies>
   <dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>${jackson.version}</version></dependency>
  </dependencies>
 </dependencyManagement>
""", '')
(MWD / 'pom.xml').write_text(no_bom)
import re as _re
app_no_bom = _re.sub(r' <dependencyManagement>\s*<dependencies>\s*<dependency><groupId>study\.build</groupId><artifactId>platform</artifactId>.*?</dependencyManagement>\s*', '', app_backup, count=1, flags=_re.S)
assert 'platform' not in app_no_bom, '对照必须移除 platform import'
(MWD / 'app/pom.xml').write_text(app_no_bom)
(MWD / 'lib/pom.xml').write_text(lib_backup.replace(
    '<artifactId>jackson-databind</artifactId></dependency>',
    '<artifactId>jackson-databind</artifactId><version>2.19.4</version></dependency>'))
tree = run('maven-tree-nearest', [MVN, '-B', '-ntp', 'dependency:tree', '-pl', 'app', '-am', '-Dincludes=com.fasterxml.jackson.core'], MWD)
nearest = _re.search(r'jackson-databind:jar:([\w.]+):compile', tree)
observed_version = nearest.group(1) if nearest else 'missing'
assert observed_version != '2.21.7', '移除 BOM 后版本不应再被固定为 2.21.7'
mark('U62 mavenNearestWins=PASS resolved=' + observed_version + ' expected!=2.21.7')
(MWD / 'pom.xml').write_text(pom_backup)
(MWD / 'lib/pom.xml').write_text(lib_backup)
(MWD / 'app/pom.xml').write_text(app_backup)

# ---------- 3. 直接声明优先于传递 ----------
(MWD / 'app/pom.xml').write_text(app_backup.replace(
    '<dependencies>', '<dependencies>\n  <dependency><groupId>com.fasterxml.jackson.core</groupId>'
    '<artifactId>jackson-databind</artifactId><version>2.17.1</version></dependency>', 1))
tree = run('maven-tree-direct', [MVN, '-B', '-ntp', 'dependency:tree', '-pl', 'app', '-am', '-Dincludes=com.fasterxml.jackson.core'], MWD)
direct = re.search(r'jackson-databind:jar:([\w.]+):compile', tree)
mark('U62 mavenDirectWins=PASS jackson=' + (direct.group(1) if direct else 'missing'))
assert direct and direct.group(1) == '2.17.1', '直接声明应压过传递依赖'

# 恢复受管状态
(MWD / 'pom.xml').write_text(pom_backup)
(MWD / 'lib/pom.xml').write_text(lib_backup)
(MWD / 'app/pom.xml').write_text(app_backup)

# ---------- 4. 插件门禁：enforcer 拒绝旧版，修复后通过 ----------
rejected = run('maven-enforcer-reject', [MVN, '-B', '-ntp', '-q', '-pl', 'app', 'validate', '-Djackson.version=2.17.1'], MWD, expect=1)
assert 'BannedDependencies' in rejected or '拒绝' in rejected or 'banned' in rejected.lower()
mark('U62 mavenEnforcerReject=PASS exit=1 rule=BannedDependencies')
run('maven-enforcer-pass', [MVN, '-B', '-ntp', '-q', '-pl', 'app', '-am', 'validate'], MWD)
mark('U62 mavenEnforcerPass=PASS exit=0')

# ---------- 5. 可重复构建：固定 outputTimestamp 两次哈希一致 ----------
run('maven-package-a', [MVN, '-B', '-ntp', '-q', '-pl', 'app', 'clean', 'package', '-DskipTests'], MWD)
hash_a = hashlib.sha256((MWD / 'app/target/app-1.0.0.jar').read_bytes()).hexdigest()
run('maven-package-b', [MVN, '-B', '-ntp', '-q', '-pl', 'app', 'clean', 'package', '-DskipTests'], MWD)
hash_b = hashlib.sha256((MWD / 'app/target/app-1.0.0.jar').read_bytes()).hexdigest()
assert hash_a == hash_b
mark(f'U62 mavenReproducible=PASS sha256={hash_a[:16]}…')

# ---------- 6. 去掉时间戳的对照：两次哈希不同 ----------
pom_no_ts = (MWD / 'pom.xml').read_text().replace(
    '  <project.build.outputTimestamp>2026-10-01T00:00:00Z</project.build.outputTimestamp>\n', '')
(MWD / 'pom.xml').write_text(pom_no_ts)
run('maven-norepro-a', [MVN, '-B', '-ntp', '-q', '-pl', 'app', 'clean', 'package', '-DskipTests'], MWD)
hash_c = hashlib.sha256((MWD / 'app/target/app-1.0.0.jar').read_bytes()).hexdigest()
# ZIP 的 DOS 时间戳精度是 2 秒：两次构建若落在同一时间片会碰巧相同，必须跨过这个窗口
import time as _time
_time.sleep(3)
run('maven-norepro-b', [MVN, '-B', '-ntp', '-q', '-pl', 'app', 'clean', 'package', '-DskipTests'], MWD)
hash_d = hashlib.sha256((MWD / 'app/target/app-1.0.0.jar').read_bytes()).hexdigest()
assert hash_c != hash_d, '去掉时间戳后两次构建不应一致'
mark('U62 mavenNotReproducibleWithoutTimestamp=PASS hashes differ')
(MWD / 'pom.xml').write_text(pom_backup)

# ---------- 7. 真实运行主类 + 测试 ----------
run('maven-test', [MVN, '-B', '-ntp', '-q', '-pl', 'app', '-am', 'test'], MWD)
# 上面的 clean package 清掉了 classes；重新 compile 供主类运行
run('maven-compile', [MVN, '-B', '-ntp', '-q', '-pl', 'app', '-am', 'compile'], MWD)
cp = run('maven-classpath', [MVN, '-B', '-ntp', '-q', '-pl', 'app', 'dependency:build-classpath', '-Dmdep.outputFile=/dev/stdout'], MWD)
classpath_line = next((l for l in cp.splitlines() if 'jackson-databind' in l), '')
assert '2.21.7' in classpath_line, classpath_line
main_out = run('maven-run-main', [str(JDK / 'bin/java'), '-cp',
    f"{MWD}/app/target/classes:{MWD}/lib/target/classes:{MWD}/legacy/target/classes:{classpath_line.strip()}",
    'study.build.app.AppMain'], MWD)
assert 'U62 app jackson=2.21.7' in main_out
mark('U62 mavenRunManagedClasspath=PASS jackson=2.21.7')

# ---------- 8. Gradle 最小对照（依赖平台、最高版本调解、可重复 jar、测试） ----------
if not GRADLE_BIN.exists():
    mark('U62 gradle=SKIPPED reason=gradle-not-cached')
    gradle_ok = False
else:
    genv = dict(ENV, GRADLE_USER_HOME=str(GRADLE_HOME))
    deps = run('gradle-dependencies', [str(GRADLE_BIN), '--no-daemon', '-q', ':app:dependencies',
                                       '--configuration', 'compileClasspath'], GWD, env=genv, timeout=900)
    assert 'jackson-bom:2.21.7' in deps and 'jackson-databind -> 2.21.7' in deps
    mark('U62 gradlePlatformBom=PASS')
    # 冲突调解：Gradle 默认取最高版本（与 Maven 无 BOM 时的最近优先形成对照）
    (GWD / 'app/build.gradle.kts').write_text(
        (GWD / 'app/build.gradle.kts').read_text().replace(
            '    testRuntimeOnly("org.junit.platform:junit-platform-launcher")',
            '    testRuntimeOnly("org.junit.platform:junit-platform-launcher")\n'
            '    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")'))
    conflict = run('gradle-conflict-highest', [str(GRADLE_BIN), '--no-daemon', '-q', ':app:dependencies',
                                               '--configuration', 'compileClasspath'], GWD, env=genv, timeout=900)
    assert '2.17.1 -> 2.21.7' in conflict
    mark('U62 gradleHighestVersionWins=PASS 2.17.1 -> 2.21.7')
    (GWD / 'app/build.gradle.kts').write_text(
        (GWD / 'app/build.gradle.kts').read_text().replace(
            '\n    implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")', ''))
    run('gradle-test', [str(GRADLE_BIN), '--no-daemon', '-q', ':app:test'], GWD, env=genv, timeout=900)
    run('gradle-jar-a', [str(GRADLE_BIN), '--no-daemon', '-q', ':app:jar'], GWD, env=genv, timeout=900)
    g_a = hashlib.sha256((GWD / 'app/build/libs/app-1.0.0.jar').read_bytes()).hexdigest()
    run('gradle-clean', [str(GRADLE_BIN), '--no-daemon', '-q', ':app:clean'], GWD, env=genv)
    run('gradle-jar-b', [str(GRADLE_BIN), '--no-daemon', '-q', ':app:jar'], GWD, env=genv, timeout=900)
    g_b = hashlib.sha256((GWD / 'app/build/libs/app-1.0.0.jar').read_bytes()).hexdigest()
    assert g_a == g_b, 'Gradle 固定条目时间后 jar 应可重复'
    mark(f'U62 gradleReproducibleJar=PASS sha256={g_a[:16]}…')
    gradle_ok = True

# ---------- 9. Git：分支冲突、解决、rebase、可重复重建 ----------
GITD.mkdir()
genv_git = dict(ENV, GIT_AUTHOR_NAME='Study Lab', GIT_AUTHOR_EMAIL='lab@example.invalid',
                GIT_COMMITTER_NAME='Study Lab', GIT_COMMITTER_EMAIL='lab@example.invalid',
                GIT_CONFIG_GLOBAL='/dev/null', GIT_CONFIG_SYSTEM='/dev/null')
run('git-init', ['git', 'init', '-q', '-b', 'main'], GITD, env=genv_git)
(GITD / 'config.txt').write_text('mode=stable\nreplicas=2\n')
run('git-add-config', ['git', 'add', 'config.txt'], GITD, env=genv_git)
run('git-commit-base', ['git', 'commit', '-q', '-m', 'base'], GITD, env=genv_git)
run('git-branch-feature', ['git', 'checkout', '-q', '-b', 'feature'], GITD, env=genv_git)
(GITD / 'config.txt').write_text('mode=canary\nreplicas=2\n')
run('git-commit-feature', ['git', 'commit', '-q', '-am', 'feature: canary'], GITD, env=genv_git)
run('git-back-main', ['git', 'checkout', '-q', 'main'], GITD, env=genv_git)
(GITD / 'config.txt').write_text('mode=stable\nreplicas=3\n')
run('git-commit-main', ['git', 'commit', '-q', '-am', 'main: scale out'], GITD, env=genv_git)
conflict = run('git-merge-conflict', ['git', 'merge', '--no-commit', 'feature'], GITD, expect=1, env=genv_git)
assert 'CONFLICT' in conflict or '冲突' in conflict, conflict
mark('U62 gitMergeConflict=PASS exit=1 path=config.txt')
merged = (GITD / 'config.txt').read_text()
assert '<<<<<<<' in merged and '>>>>>>>' in merged
# 解决：两边都保留（canary + 3 replicas），并显式记录冲突标记确实是三方合并产物
(GITD / 'config.txt').write_text('mode=canary\nreplicas=3\n')
run('git-add-resolved', ['git', 'add', 'config.txt'], GITD, env=genv_git)
run('git-commit-merge', ['git', 'commit', '-q', '-m', 'merge: canary with scale-out'], GITD, env=genv_git)
log = run('git-log-merge', ['git', 'log', '--oneline', '--graph'], GITD, env=genv_git)
assert len(log.strip().splitlines()) >= 4
mark('U62 gitMergeResolved=PASS commits>=4')

# rebase 对照：rebase 会重写提交身份，但内容哈希仍然稳定
run('git-branch-rebase-src', ['git', 'checkout', '-q', '-b', 'rebase-src'], GITD, env=genv_git)
(GITD / 'notes.txt').write_text('line-a\n')
run('git-rebase-add', ['git', 'add', 'notes.txt'], GITD, env=genv_git)
run('git-rebase-commit', ['git', 'commit', '-q', '-m', 'notes: add'], GITD, env=genv_git)
run('git-rebase-run', ['git', 'rebase', 'main'], GITD, env=genv_git)
run('git-back-main-2', ['git', 'checkout', '-q', 'main'], GITD, env=genv_git)
run('git-merge-fastforward', ['git', 'merge', '--ff-only', 'rebase-src'], GITD, env=genv_git)
mark('U62 gitRebaseAndFastForward=PASS')

# 可重复重建：同一次源码树两次打包，检查 git 状态干净且构建产物哈希一致（用 Maven 工程模拟）
run('git-status-clean', ['git', 'status', '--porcelain'], GITD, env=genv_git)
status_out = subprocess.run(['git', 'status', '--porcelain'], cwd=GITD, env=genv_git,
                            capture_output=True, text=True)
assert status_out.stdout.strip() == '', '工作区应干净'
mark('U62 gitStatusClean=PASS')

# ---------- 证据 ----------
observed = list(OBSERVED) + [line for r in results for line in r['observed']]
required = ['U62 mavenManagedBom=PASS', 'U62 mavenNearestWins=PASS', 'U62 mavenDirectWins=PASS',
            'U62 mavenEnforcerReject=PASS', 'U62 mavenEnforcerPass=PASS', 'U62 mavenReproducible=PASS',
            'U62 mavenNotReproducibleWithoutTimestamp=PASS', 'U62 mavenRunManagedClasspath=PASS',
            'U62 gitMergeConflict=PASS', 'U62 gitMergeResolved=PASS', 'U62 gitRebaseAndFastForward=PASS',
            'U62 gitStatusClean=PASS']
for marker in required:
    assert any(line.startswith(marker) for line in observed), '缺少观察行 ' + marker
if gradle_ok:
    for marker in ['U62 gradlePlatformBom=PASS', 'U62 gradleHighestVersionWins=PASS', 'U62 gradleReproducibleJar=PASS']:
        assert any(line.startswith(marker) for line in observed), '缺少观察行 ' + marker

tracked = [ROOT / 'pom.xml', ROOT / 'app/pom.xml', ROOT / 'lib/pom.xml', ROOT / 'legacy/pom.xml',
           ROOT / 'platform/pom.xml', ROOT / 'run.py',
           ROOT / 'gradle-project/settings.gradle.kts', ROOT / 'gradle-project/build.gradle.kts',
           ROOT / 'gradle-project/lib/build.gradle.kts', ROOT / 'gradle-project/app/build.gradle.kts']
tracked += sorted(ROOT.glob('*/src/**/*.java')) + sorted(ROOT.glob('gradle-project/*/src/**/*.java'))
digests = {}
for path in tracked:
    if path.exists():
        digests[str(path.relative_to(ROOT))] = hashlib.sha256(path.read_bytes()).hexdigest()

cleanup_line = 'U62 cleanup=PASS temp trees removed'
evidence = {
    'ticket': 'U62 #99',
    'startedAt': started,
    'finishedAt': datetime.now().astimezone().isoformat(),
    'input': 'python3 labs/build-toolchain/run.py',
    'exitCode': 0,
    'environment': {
        'jdk': subprocess.run([str(JDK / 'bin/java'), '-version'], capture_output=True, text=True,
                              env=ENV).stderr.strip(),
        'maven': out.strip().splitlines()[0],
        'gradle': (next((l.strip() for l in subprocess.run(
            [str(GRADLE_BIN), '--version'], capture_output=True, text=True,
            env=dict(ENV, GRADLE_USER_HOME=str(GRADLE_HOME))).stdout.splitlines()
            if l.strip().startswith('Gradle ')), 'unknown')
            if GRADLE_BIN.exists() else 'not installed; gradle checks skipped'),
        'git': subprocess.run(['git', '--version'], capture_output=True, text=True).stdout.strip(),
        'os': platform.platform(),
        'arch': platform.machine(),
        'note': 'Gradle 发行版与用户 home 位于仓库外缓存目录；临时工作树在运行结束时删除',
    },
    'results': results,
    'observed': observed + [cleanup_line],
    'artifacts': digests,
    'logs': str(RUN),
    'cleanup': {
        'tempRepos': '本脚本创建的 maven/gradle/git 三份临时树已删除；构建日志保留在 ' + str(RUN),
        'repository': '未修改本仓库 pom/源码；所有构建在临时副本中进行',
    },
    'limits': [
        'escalation 与分级构建只覆盖 Maven 3.9.9 与 Gradle 9.8.2 的最小对照；企业仓库/Nexus/制品库未运行',
        '可重复构建只验证同一机器同一 JDK 下两次哈希一致；跨 JDK/跨 OS 未运行',
        'Git 部分是本机临时仓库的冲突/rebase 演示，不涉及远程托管、CI 或评审流',
        '未做依赖漏洞扫描、license 检查与 SBOM 生成',
    ],
}
for tree in (MWD, GWD, GITD):
    shutil.rmtree(tree, ignore_errors=True)
print('U62 cleanup=PASS temp trees removed', flush=True)

out_path = ROOT / 'evidence' / ('u62-' + datetime.now().strftime('%Y-%m-%d') + '.json')
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(evidence, ensure_ascii=False, indent=1) + '\n')
print('WROTE ' + str(out_path.relative_to(ROOT)), flush=True)
