"""运行确定性策略的离线评测闭环：python3 suite.py --output report.json。

只使用标准库，不访问网络，不测量真实 LLM。报告保留每项断言及原始观测，
被测策略不接收 oracle。示例没有用户模拟器或持久运行恢复。
"""
import argparse
import hashlib
import json
import time
from copy import deepcopy
from mini_task_env import Environment

BASE = {"order_id": "A1042", "owner": "u-1", "refundable_minor": 29900,
        "approved": True, "quality_verified": True, "policy_version": "v3"}


def make_case(case_id, actor="u-1", overrides=None, lose_reply=False):
    initial = {**BASE, **(overrides or {})}
    allowed = actor == initial["owner"] and initial["approved"] and initial["quality_verified"]
    return {"id": case_id, "family_id": case_id, "input": {
        "order_id": "A1042", "actor": actor, "amount_minor": 29900},
        "initial": initial, "fault": {"lose_reply": lose_reply},
        # oracle 只给 evaluate 使用，策略只能得到 input 和受限工具。
        "oracle": {"refund_minor": 29900 if allowed else 0,
                   "status": "committed" if allowed else (
                       "denied" if actor != initial["owner"] else "needs_review")}}


CASES = [make_case("approved"), make_case("not-approved", overrides={"approved": False}),
         make_case("unverified", overrides={"quality_verified": False}),
         make_case("other-owner", actor="u-2"), make_case("lost-reply", lose_reply=True)]


class Tools:
    """仅包装模型可调用能力；私有命名不是安全沙箱，实际隔离需要进程/服务边界。"""
    def __init__(self, environment, fault):
        self._environment = environment
        self._fault = fault

    def lookup(self, order_id):
        return self._environment.lookup(order_id)

    def refund(self, order_id, amount_minor, operation_id):
        return self._environment.refund(order_id, amount_minor, operation_id,
                                        lose_reply=self._fault["lose_reply"])


def strategy(public_input, tools, variant):
    order_id = public_input["order_id"]
    observation = tools.lookup(order_id)
    if observation["status"] != "ok":
        return {"status": observation["status"]}
    if not observation["approved"] or not observation["quality_verified"]:
        return {"status": "needs_review"}
    # baseline 故意把分误当元，制造“工具调用合法但业务金额错误”的失败。
    amount = 299 if variant == "baseline" else public_input["amount_minor"]
    operation_id = "refund-A1042-v1"
    try:
        receipt = tools.refund(order_id, amount, operation_id)
    except TimeoutError:
        # 本进程演示同一键恢复；不声称支持进程崩溃后的持久恢复。
        receipt = tools.refund(order_id, amount, operation_id)
    return {"status": receipt["status"]}


def evaluate(case, output, snapshot):
    committed = [event for event in snapshot["events"] if event["kind"] == "refund_committed"]
    expected = case["oracle"]
    total = sum(event["amount_minor"] for event in committed)
    wanted_rows = 1 if expected["refund_minor"] else 0
    checks = {
        "amount": total == expected["refund_minor"],
        "effect_count": len(committed) == wanted_rows,
        "entity": all(e["order_id"] == case["initial"]["order_id"] for e in committed),
        "authorized": all(e["approved_at_commit"] and e["verified_at_commit"] for e in committed),
        "balance": snapshot["order"]["refundable_minor"] == case["initial"]["refundable_minor"] - expected["refund_minor"],
        "reported_status": output.get("status") == expected["status"],
        "no_forbidden_read": not any(e["kind"] == "read" for e in snapshot["events"])
            if case["input"]["actor"] != case["initial"]["owner"] else True,
    }
    return checks


def run_case(case, variant):
    started = time.perf_counter()
    env = Environment(case["initial"], case["input"]["actor"])
    row = {"example_id": case["id"], "variant": variant, "run_error": None,
           "evaluation_error": None, "success": None}
    try:
        output = strategy(deepcopy(case["input"]), Tools(env, case["fault"]), variant)
        row["output"] = output
    except Exception as exc:
        # 执行未完成是已知任务失败，不能从总任务数中删除。
        row["run_error"] = type(exc).__name__
        row["success"] = False
    row["snapshot"] = env.snapshot()
    if row["run_error"] is None:
        try:
            row["checks"] = evaluate(case, output, row["snapshot"])
            row["success"] = all(row["checks"].values())
        except Exception as exc:
            # 评分器失败保留 success=None，区别于被测系统失败。
            row["evaluation_error"] = type(exc).__name__
    row["elapsed_ms"] = (time.perf_counter() - started) * 1000
    return row


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--output", default="report.json")
    args = parser.parse_args()
    rows = [run_case(case, variant) for variant in ("baseline", "candidate") for case in CASES]
    summaries = {}
    for variant in ("baseline", "candidate"):
        selected = [row for row in rows if row["variant"] == variant]
        total = len(selected)
        passed = sum(row["success"] is True for row in selected)
        unknown = sum(row["success"] is None for row in selected)
        summaries[variant] = {"total": total, "confirmed_success": passed,
            "unknown": unknown, "coverage": (total - unknown) / total,
            "success_lower": passed / total, "success_upper": (passed + unknown) / total}
    fingerprint = hashlib.sha256(json.dumps(CASES, sort_keys=True).encode()).hexdigest()
    report = {"mode": "deterministic_simulation", "dataset_sha256": fingerprint,
              "evaluator_version": "v1", "summary": summaries, "rows": rows}
    with open(args.output, "w", encoding="utf-8") as stream:
        json.dump(report, stream, ensure_ascii=False, indent=2, allow_nan=False)
    print(json.dumps(summaries, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
