#!/usr/bin/env python3
"""End-to-end offline test for bash/run.py + collect_results + perf_backup.

This script does NOT call any model API. It uses an existing benchmark
directory (aime24/seed_42) and walks through every code path that the real
``run.py`` would touch:

1. backup_perf_stats — confirm a perf summary snapshot is written
2. archive_predictions — confirm per-sample records are deduplicated into
   the archive
3. restore_from_backup — confirm a fresh work_dir can be rehydrated from
   the backup files
4. collect_results aggregation — confirm correct metrics are produced
5. Whitelist mode — confirm only the requested benchmarks appear in the
   summary
6. hle_low alias — confirm hle_low directory maps to canonical "hle"
7. Reset recovery — confirm that even after wiping the predictions file
   and the report summary, the archive+backup still recover the metrics

Run from the project root:

    python bash/test_e2e.py
"""

import json
import shutil
import sys
import tempfile
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))

import collect_results as cr  # noqa: E402
import perf_backup as pb  # noqa: E402


PROJECT_ROOT = Path('/data1/sora/evalscope')
SOURCE_OUTPUT = PROJECT_ROOT / 'output'


def banner(msg):
    print('\n' + '=' * 60)
    print(f' {msg}')
    print('=' * 60)


def assert_close(actual, expected, name, tol=1e-3):
    if abs(actual - expected) > tol:
        raise AssertionError(f'{name}: expected ~{expected}, got {actual}')
    print(f'  OK  {name}={actual:.4f}')


def main():
    banner('Setting up isolated test output dir')
    test_root = PROJECT_ROOT / 'output_e2e_test'
    if test_root.exists():
        shutil.rmtree(test_root)
    test_root.mkdir(parents=True)

    # Copy the canonical aime24 data into the isolated dir
    src = SOURCE_OUTPUT / 'aime24' / 'seed_42'
    dst = test_root / 'aime24' / 'seed_42'
    shutil.copytree(src, dst)

    # And copy hle_low as hle_low under the test dir (so we exercise the
    # alias code path).
    hle_src = SOURCE_OUTPUT / 'hle_low' / 'seed_42'
    hle_dst = test_root / 'hle_low' / 'seed_42'
    if hle_src.exists():
        shutil.copytree(hle_src, hle_dst)

    benchmark = 'aime24'
    model_name = 'DeepSeek-V4-Flash-Int8'

    banner('1) backup_perf_stats — snapshot summary')
    report_json = dst / 'reports' / model_name / f'{benchmark}.json'
    backup_path = pb.backup_perf_stats(test_root, benchmark, model_name, report_json)
    assert backup_path.exists(), 'backup file was not created'
    payload = json.loads(backup_path.read_text())
    assert payload['n_samples'] == 30, f'expected 30 samples, got {payload["n_samples"]}'
    print(f'  OK  backup written: {backup_path}, n_samples={payload["n_samples"]}')

    banner('2) archive_predictions — deduplicate into archive')
    preds = dst / 'predictions' / model_name
    archive_path = pb.archive_predictions(test_root, benchmark, model_name, preds)
    assert archive_path.exists(), 'archive file was not created'
    n_lines = sum(1 for line in archive_path.read_text().splitlines() if line.strip())
    assert n_lines == 30, f'expected 30 archived samples, got {n_lines}'
    print(f'  OK  archive written: {archive_path}, samples={n_lines}')

    banner('3) restore_from_backup — rehydrate a fresh work_dir')
    fresh_dir = test_root / 'fresh_work' / 'seed_42'
    fresh_report = fresh_dir / 'reports' / model_name / f'{benchmark}.json'
    fresh_preds = fresh_dir / 'predictions' / model_name
    # Pre-create empty structure to mimic what eval_task would do
    fresh_report.parent.mkdir(parents=True, exist_ok=True)
    fresh_report.write_text(json.dumps({
        'name': f'{model_name}@{benchmark}',
        'perf_metrics': {'summary': {'n_samples': 1}},
    }))
    restored = pb.restore_from_backup(
        test_root, benchmark, model_name, fresh_preds, fresh_report,
    )
    assert restored, 'restore_from_backup returned False'
    restored_report = json.loads(fresh_report.read_text())
    assert restored_report['perf_metrics']['summary']['n_samples'] == 30, \
        f'restored n_samples={restored_report["perf_metrics"]["summary"]["n_samples"]}'
    print('  OK  restored report n_samples=30 (matches backup)')

    banner('4) collect_results — aggregate metrics')
    csv, xlsx = cr.eval_benchmark([benchmark], test_root, model_name)
    import pandas as pd
    df = pd.read_csv(csv)
    aime24 = df[df['Benchmark'] == 'aime24'].iloc[0]
    # Single seed_42 run, so values match the report directly (no multi-run
    # averaging). The full-summary CSV averages across run_1..run_11.
    assert_close(aime24['得分'], 0.6333, 'aime24 score (single seed)')
    assert_close(aime24['实测时间(h)'], 0.1469, 'aime24 duration (h)')
    assert_close(aime24['总样本数'], 30, 'aime24 sample count')
    assert_close(aime24['延迟_mean(s)'], 35.24971, 'aime24 latency mean')
    assert_close(aime24['TTFT_mean(s)'], 0.26301, 'aime24 TTFT mean')
    assert_close(aime24['TPOT_mean(s)'], 0.02165, 'aime24 TPOT mean')
    assert_close(aime24['输入tokens_mean'], 119.33, 'aime24 input_tokens_mean')
    assert_close(aime24['输出tokens_mean'], 1609.8, 'aime24 output_tokens_mean')

    banner('5) Whitelist mode — only the requested benchmarks appear')
    csv2, _ = cr.eval_benchmark([benchmark], test_root, model_name)
    df2 = pd.read_csv(csv2)
    assert df2['Benchmark'].dropna().tolist() == ['aime24'], \
        f'whitelist did not limit benchmarks: {df2["Benchmark"].tolist()}'
    print('  OK  summary only contains aime24 + total')

    banner('6) hle_low → hle alias')
    if hle_src.exists():
        csv3, _ = cr.eval_benchmark(['hle'], test_root, model_name)
        df3 = pd.read_csv(csv3)
        assert 'hle' in df3['Benchmark'].tolist(), \
            f'hle canonical name missing from: {df3["Benchmark"].tolist()}'
        assert 'hle_low' not in df3['Benchmark'].tolist(), \
            f'hle_low should be aliased away: {df3["Benchmark"].tolist()}'
        print('  OK  hle_low directory maps to canonical "hle"')

    banner('7) Reset recovery — wipe report+predictions, expect archive to restore')
    # Wipe the current predictions
    shutil.rmtree(preds)
    # Reset report.json
    data = json.loads(report_json.read_text())
    data['perf_metrics']['summary']['n_samples'] = 1
    data['perf_metrics']['summary']['latency']['mean'] = 0.0
    report_json.write_text(json.dumps(data, indent=2))

    csv4, _ = cr.eval_benchmark([benchmark], test_root, model_name)
    df4 = pd.read_csv(csv4)
    aime24_after = df4[df4['Benchmark'] == 'aime24'].iloc[0]
    assert_close(aime24_after['得分'], 0.6333, 'aime24 score (post-reset)')
    assert_close(aime24_after['总样本数'], 30, 'aime24 sample count (post-reset)')
    assert_close(aime24_after['延迟_mean(s)'], 35.24971, 'aime24 latency mean (post-reset)')
    assert_close(aime24_after['TTFT_mean(s)'], 0.26301, 'aime24 TTFT mean (post-reset)')
    print('  OK  archive fully recovered metrics after predictions wipe')

    banner('8) Backup monotonicity — new backup must not overwrite a larger one')
    # We have a backup with n_samples=30. Now write a fresh report with
    # n_samples=2 and call backup_perf_stats again — the backup should NOT
    # be overwritten.
    data = json.loads(report_json.read_text())
    data['perf_metrics']['summary']['n_samples'] = 2
    report_json.write_text(json.dumps(data, indent=2))
    pb.backup_perf_stats(test_root, benchmark, model_name, report_json)
    payload2 = json.loads(backup_path.read_text())
    assert payload2['n_samples'] == 30, \
        f'backup was clobbered: n_samples now {payload2["n_samples"]}'
    print('  OK  backup preserved n_samples=30 even after a reset run')

    banner('All checks passed')
    print(f'Test artifacts under {test_root} (kept for inspection)')


if __name__ == '__main__':
    main()
