"""agent_eval 框架单测:用 mock 跑/收产物/judge,验证行为回归评估逻辑(可进 CI)。

真实 LLM 评估由 evals/run_evals.py 手动/nightly 触发(花钱、要 key),不在默认门禁。
"""
from __future__ import annotations

from agentpaas.engine.agent_eval import EvalTask, run_eval_task, summarize
from agentpaas.engine.pipeline import JudgeResult


def _judge(score):
    return lambda req: JudgeResult(score=score, passed=score >= req.threshold,
                                   reasons=["mock"], raw="", usage={})


def _run(output="done", steps=10, cost=0.1, ws="/ws"):
    return lambda task: {"output": output, "steps": steps, "cost_usd": cost, "workspace_path": ws}


def test_all_good_passes():
    task = EvalTask(id="t1", input="x", must_produce=["report.md"], rubric="好不好", threshold=0.6)
    r = run_eval_task(task, run_fn=_run(),
                      collect_artifacts_fn=lambda ws: {"report.md": "完整内容"},
                      judge_fn=_judge(0.8))
    assert r.files_ok and r.judge_passed and r.passed
    assert r.judge_score == 0.8 and r.steps == 10


def test_missing_file_fails():
    task = EvalTask(id="t2", must_produce=["paper.pdf", "report.md"], rubric="", threshold=0.6)
    r = run_eval_task(task, run_fn=_run(),
                      collect_artifacts_fn=lambda ws: {"report.md": "x"},
                      judge_fn=_judge(1.0))
    assert not r.files_ok
    assert r.missing_files == ["paper.pdf"]
    assert not r.passed


def test_judge_below_threshold_fails():
    task = EvalTask(id="t3", must_produce=[], rubric="质量", threshold=0.7)
    r = run_eval_task(task, run_fn=_run(),
                      collect_artifacts_fn=lambda ws: {"a.md": "敷衍"},
                      judge_fn=_judge(0.4))
    assert not r.judge_passed and not r.passed


def test_error_output_fails():
    task = EvalTask(id="t4", rubric="", must_produce=[])
    r = run_eval_task(task, run_fn=_run(output="[DASHSCOPE_ERROR] boom"),
                      collect_artifacts_fn=lambda ws: {},
                      judge_fn=_judge(1.0))
    assert r.error and not r.passed


def test_run_exception_captured():
    def boom(task):
        raise RuntimeError("run crashed")
    task = EvalTask(id="t5")
    r = run_eval_task(task, run_fn=boom,
                      collect_artifacts_fn=lambda ws: {}, judge_fn=_judge(1.0))
    assert "run crashed" in r.error and not r.passed


def test_no_rubric_skips_judge():
    task = EvalTask(id="t6", must_produce=["x.md"], rubric="")
    r = run_eval_task(task, run_fn=_run(),
                      collect_artifacts_fn=lambda ws: {"x.md": "ok"},
                      judge_fn=lambda req: (_ for _ in ()).throw(AssertionError("不该调 judge")))
    assert r.judge_passed and r.passed  # 无 rubric → 不调 judge,judge_passed 默认 True


def test_summarize():
    task = EvalTask(id="a", must_produce=["r.md"], rubric="", )
    good = run_eval_task(task, run_fn=_run(steps=5, cost=0.2),
                         collect_artifacts_fn=lambda ws: {"r.md": "x"}, judge_fn=_judge(1))
    bad = run_eval_task(EvalTask(id="b", must_produce=["miss.pdf"]),
                        run_fn=_run(steps=9, cost=0.3),
                        collect_artifacts_fn=lambda ws: {}, judge_fn=_judge(1))
    s = summarize([good, bad])
    assert s["total"] == 2 and s["passed"] == 1 and s["pass_rate"] == 0.5
    assert s["failures"] == ["b"]
    assert s["total_cost_usd"] == 0.5
