from __future__ import annotations

import re
from datetime import UTC, datetime, timedelta
from decimal import Decimal

from sqlalchemy import create_engine, or_
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

from app.core.agent_enums import AgentAssetType
from app.db.base import Base
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.schemas.ontology import OntologyParseRequest
from app.schemas.user_agent import UserAgentCitation, UserAgentRequest, UserAgentReviewRiskBrief
from app.services.agent_assets import AgentAssetService
from app.services.application_location_semantics import resolve_jieba_tokens
from app.services.ontology import SemanticOntologyService
from app.services.user_agent import UserAgentService
from app.services.user_agent_documents import UserAgentDocumentService


def build_session_factory() -> sessionmaker[Session]:
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    return sessionmaker(bind=engine, autoflush=False, autocommit=False)


def application_claim_query(db: Session):
    return db.query(ExpenseClaim).filter(
        or_(
            ExpenseClaim.claim_no.like("AP-%"),
            ExpenseClaim.claim_no.like("APP-%"),
            ExpenseClaim.claim_no.like("A________"),
        )
    )


def build_application_user_agent_response(
    db: Session,
    message: str,
    *,
    history: list[dict[str, object]] | None = None,
    context_overrides: dict[str, object] | None = None,
):
    context_json = {
        "session_type": "application",
        "entry_source": "application",
        "attachment_count": 0,
    }
    if context_overrides:
        context_json.update(context_overrides)
    if history is not None:
        context_json["conversation_history"] = history
    ontology = SemanticOntologyService(db).parse(
        OntologyParseRequest(
            query=message,
            user_id="pytest",
            context_json=context_json,
        )
    )
    return UserAgentService(db).respond(
        UserAgentRequest(
            run_id=ontology.run_id,
            user_id="pytest",
            message=message,
            ontology=ontology,
            context_json=context_json,
            tool_payload={"clarification_required": ontology.clarification_required},
        )
    )


def test_user_agent_query_returns_readable_answer_and_actions() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="张三 4 月差旅报销金额是多少",
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="张三 4 月差旅报销金额是多少",
                ontology=ontology,
                tool_payload={"record_count": 2, "total_amount": 8800.0},
            )
        )

        assert "8800.00" in response.answer
        assert len(response.suggested_actions) >= 1


def test_user_agent_returns_readable_query_answer_when_runtime_model_is_skipped(monkeypatch) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="张三 4 月差旅报销金额是多少",
                user_id="pytest",
            )
        )
        service = UserAgentService(db)
        monkeypatch.setattr(service, "_generate_answer_with_model", lambda *args, **kwargs: "这是模型回答")

        response = service.respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="张三 4 月差旅报销金额是多少",
                ontology=ontology,
                tool_payload={"record_count": 2, "total_amount": 8800.0},
            )
        )

        assert "共 2 笔" in response.answer
        assert "8800.00" in response.answer


def test_user_agent_sanitizes_model_thinking_blocks() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        service = UserAgentService(db)

        assert (
            service._sanitize_model_answer("<think>内部推理</think>\n最终答复")
            == "最终答复"
        )


def test_user_agent_rejects_visible_reasoning_drafts() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        service = UserAgentService(db)

        assert (
            service._sanitize_model_answer(
                "用户问的是：住宿费怎么算？\n让我分析一下：\n1. 实体识别..."
            )
            is None
        )


def test_user_agent_knowledge_prompt_enforces_knowledge_boundary() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="住宿费标准是多少？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)
        messages = service._build_model_messages(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="住宿费标准是多少？",
                ontology=ontology,
                tool_payload={"result_type": "knowledge_search", "hits": []},
            ),
            citations=[],
            suggested_actions=[],
            risk_flags=[],
            draft_payload=None,
            fallback_answer="",
        )
        assert "只能依据 facts.tool_payload.hits、facts.knowledge_answer_evidence" in messages[0]["content"]
        assert "不能用常识、外部知识或主观推断补齐缺失条件" in messages[0]["content"]
        assert "不能只依赖排在最前面的片段" in messages[0]["content"]
        assert "不能把第一列的数值直接套给后面的列名" in messages[0]["content"]
        assert "最终答复必须使用 Markdown" in messages[0]["content"]
        assert "## 结论" in messages[0]["content"]
        assert "如果不能，必须明确说当前知识库没有找到直接依据" in messages[0]["content"]
        assert "禁止使用“已命中”“答案整理阶段”“稍后重试”" in messages[0]["content"]
        assert "knowledge_evidence_blocks" in messages[0]["content"]
        assert '"knowledge_answer_evidence": []' in messages[1]["content"]


def test_user_agent_application_context_uses_application_language() -> None:
    session_factory = build_session_factory()
    message = (
        "发生时间：2026-05-25\n"
        "地点：上海\n"
        "事由：支持上海国网服务器部署\n"
        "天数：3天"
    )
    context_json = {
        "session_type": "application",
        "entry_source": "application",
        "attachment_count": 0,
    }
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=message,
                ontology=ontology,
                context_json=context_json,
                tool_payload={"clarification_required": True},
            )
        )

        assert "费用申请" in response.answer
        assert "| 字段 | 内容 |" in response.answer
        assert "| 出发时间 | 2026-05-25 |" in response.answer
        assert "| 返回时间 | 2026-05-27 |" in response.answer
        assert "支持上海国网服务器部署" in response.answer
        assert "当前还需要补充：出行方式" in response.answer
        assert "请先在下面选择报销场景" not in response.answer
        assert response.review_payload is None
        assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
        assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式："


def test_user_agent_application_infers_natural_reason_and_expands_single_date() -> None:
    session_factory = build_session_factory()
    message = "发生时间：2026-05-25\n去上海出差3天，支撑上海国网服务器部署"
    with session_factory() as db:
        response = build_application_user_agent_response(db, message)

        assert "| 出发时间 | 2026-05-25 |" in response.answer
        assert "| 返回时间 | 2026-05-27 |" in response.answer
        assert "| 地点 | 上海市 |" in response.answer
        assert "| 事由 | 支撑上海国网服务器部署 |" in response.answer
        assert "当前还需要先补充：申请事由" not in response.answer
        assert "当前还需要补充：出行方式" in response.answer
        assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]


def test_user_agent_application_normalizes_location_to_region_city() -> None:
    session_factory = build_session_factory()
    yili_message = (
        "发生时间：2026-05-25\n"
        "地点：伊犁\n"
        "事由：支撑新疆电力仿生产部署\n"
        "天数：3天"
    )
    beijing_message = (
        "发生时间：2026-05-25\n"
        "地点：北京\n"
        "事由：支撑总部系统部署\n"
        "天数：3天"
    )
    with session_factory() as db:
        yili_response = build_application_user_agent_response(db, yili_message)
        beijing_response = build_application_user_agent_response(db, beijing_message)

        assert "| 出发时间 | 2026-05-25 |" in yili_response.answer
        assert "| 返回时间 | 2026-05-27 |" in yili_response.answer
        assert "| 地点 | 新疆，伊犁 |" in yili_response.answer
        assert "| 事由 | 支撑新疆电力仿生产部署 |" in yili_response.answer
        assert "伊犁出差" not in yili_response.answer
        assert "| 地点 | 北京市 |" in beijing_response.answer


def test_user_agent_application_uses_selected_time_and_natural_language_fields() -> None:
    session_factory = build_session_factory()
    message = "出差上海，支撑国网服务器上线部署"
    context_json = {
        "session_type": "application",
        "entry_source": "application",
        "business_time_context": {
            "mode": "single",
            "start_date": "2026-05-25",
            "end_date": "2026-05-25",
            "display_value": "2026-05-25",
        },
    }
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=message,
                ontology=ontology,
                context_json=context_json,
                tool_payload={"clarification_required": True},
            )
        )

        assert "| 出发时间 | 2026-05-25 |" in response.answer
        assert "| 返回时间 | 2026-05-25 |" in response.answer
        assert "| 地点 | 上海市 |" in response.answer
        assert "| 事由 | 支撑国网服务器上线部署 |" in response.answer
        assert "当前还需要补充：出行方式" in response.answer
        assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
        assert response.suggested_actions[0].action_type == "prefill_composer"
        assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式："


def test_user_agent_application_builds_system_estimate_after_transport_choice() -> None:
    session_factory = build_session_factory()
    initial_message = (
        "发生时间：2026-05-25\n"
        "地点：上海\n"
        "事由：支持上海国网服务器部署\n"
        "天数：3天"
    )
    with session_factory() as db:
        response = build_application_user_agent_response(
            db,
            "飞机",
            history=[{"role": "user", "content": initial_message}],
        )

        assert "这是费用申请核对结果" in response.answer
        assert "| 出行方式 | 飞机 |" in response.answer
        assert "| 系统预估费用 |" in response.answer
        assert "交通" in response.answer
        assert "| 交通费用口径 | 预估交通费用 2,330元 |" in response.answer
        assert "2,330元" in response.answer
        assert "参考票价" not in response.answer
        assert "查询耗时" not in response.answer
        assert response.requires_confirmation is True
        assert response.suggested_actions == []


def test_user_agent_application_uses_selected_date_range_and_keeps_reason() -> None:
    session_factory = build_session_factory()
    message = "去上海出差4天，支撑国网仿生产环境部署，飞机"
    context_json = {
        "session_type": "application",
        "entry_source": "application",
        "business_time_context": {
            "mode": "range",
            "start_date": "2026-02-20",
            "end_date": "2026-02-23",
            "display_value": "2026-02-20 至 2026-02-23",
        },
        "name": "曹笑竹",
        "department_name": "技术部",
        "position": "财务智能化产品经理",
        "manager_name": "向万红",
        "grade": "P5",
    }
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=message,
                ontology=ontology,
                context_json=context_json,
                tool_payload={"clarification_required": ontology.clarification_required},
            )
        )

        assert "| 出发时间 | 2026-02-20 |" in response.answer
        assert "| 返回时间 | 2026-02-23 |" in response.answer
        assert "| 地点 | 上海市 |" in response.answer
        assert "| 事由 | 支撑国网仿生产环境部署 |" in response.answer
        assert "| 天数 | 4天 |" in response.answer
        assert "| 住宿上限/天 | 450元/天 |" in response.answer
        assert "| 补贴标准/天 | 100元/天 |" in response.answer
        assert "| 规则测算参考 | 交通 2,460元 + 住宿 1,800元 + 补贴 400元 = 4,660元（4天） |" in response.answer
        assert "| 发生时间 |" not in response.answer
        assert "| 事由 | 2026-02-20 至 2026-02-23 |" not in response.answer


def test_user_agent_application_keeps_labeled_reason_in_structured_travel_form() -> None:
    session_factory = build_session_factory()
    message = (
        "发生时间：2026-02-20 至 2026-02-23\n"
        "地点：上海\n"
        "事由：支撑国网仿生产环境建设\n"
        "天数：4天"
    )
    context_json = {
        "session_type": "application",
        "entry_source": "application",
        "name": "曹笑竹",
        "department_name": "技术部",
        "position": "财务智能化产品经理",
        "manager_name": "向万红",
        "grade": "P5",
    }
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest-structured-application-reason@example.com",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-structured-application-reason@example.com",
                message=message,
                ontology=ontology,
                context_json=context_json,
                tool_payload={"clarification_required": ontology.clarification_required},
            )
        )

        assert "| 申请类型 | 差旅费用申请 |" in response.answer
        assert "| 出发时间 | 2026-02-20 |" in response.answer
        assert "| 返回时间 | 2026-02-23 |" in response.answer
        assert "| 地点 | 上海市 |" in response.answer
        assert "| 事由 | 支撑国网仿生产环境建设 |" in response.answer
        assert "| 天数 | 4天 |" in response.answer
        assert "申请事由" not in response.answer
        assert "当前还需要补充：出行方式" in response.answer


def test_user_agent_application_derives_days_from_selected_date_range() -> None:
    session_factory = build_session_factory()
    message = "去上海出差，支撑国网仿生产服务器部署，火车"
    context_json = {
        "session_type": "application",
        "entry_source": "application",
        "business_time_context": {
            "mode": "range",
            "start_date": "2026-02-20",
            "end_date": "2026-02-23",
            "display_value": "2026-02-20 至 2026-02-23",
        },
        "application_preview": {
            "fields": {
                "applicationType": "差旅费用申请",
                "time": "2026-02-20 至 2026-02-23",
                "location": "上海市",
                "reason": "支撑国网仿生产服务器部署",
                "days": "待补充",
                "transportMode": "火车",
                "grade": "P5",
            }
        },
        "name": "曹笑竹",
        "department_name": "技术部",
        "position": "财务智能化产品经理",
        "manager_name": "向万红",
        "grade": "P5",
    }
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=message,
                ontology=ontology,
                context_json=context_json,
                tool_payload={"clarification_required": ontology.clarification_required},
            )
        )

        assert "| 出发时间 | 2026-02-20 |" in response.answer
        assert "| 返回时间 | 2026-02-23 |" in response.answer
        assert "| 天数 | 4天 |" in response.answer
        assert "| 天数 | 待补充 |" not in response.answer
        assert "（4天）" in response.answer
        assert "（1天）" not in response.answer


def test_user_agent_application_missing_base_actions_prefill_composer() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        response = build_application_user_agent_response(
            db,
            "地点：上海\n事由：支撑国网服务器部署\n天数：3天",
        )

        assert "当前还需要补充：出行方式" in response.answer
        assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
        assert response.suggested_actions[0].action_type == "prefill_composer"
        assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式："


def test_user_agent_application_precomputes_time_from_today_and_days() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        response = build_application_user_agent_response(
            db,
            "去北京出差3天，支撑国网仿生产环境部署，飞机，预计费用12000元",
            context_overrides={
                "client_now_iso": "2026-05-28T16:30:00.000Z",
                "client_timezone_offset_minutes": -480,
            },
        )

        assert "这是费用申请核对结果" in response.answer
        assert "| 出发时间 | 2026-05-29 |" in response.answer
        assert "| 返回时间 | 2026-05-31 |" in response.answer
        assert response.requires_confirmation is True


def test_user_agent_application_builds_preview_when_amount_is_ready() -> None:
    session_factory = build_session_factory()
    initial_message = (
        "发生时间：2026-05-25\n"
        "地点：上海\n"
        "事由：支持上海国网服务器部署\n"
        "天数：3天"
    )
    with session_factory() as db:
        response = build_application_user_agent_response(
            db,
            "预计总费用：12000元",
            context_overrides={
                "name": "张三",
                "department_name": "交付部",
                "position": "实施经理",
                "manager_name": "李文静",
            },
            history=[
                {"role": "user", "content": initial_message},
                {"role": "user", "content": "飞机"},
            ],
        )

        assert "这是费用申请核对结果" in response.answer
        assert "| 字段 | 内容 |" in response.answer
        assert "| 姓名 | 张三 |" in response.answer
        assert "| 部门 | 交付部 |" in response.answer
        assert "| 岗位 | 实施经理 |" in response.answer
        assert "| 直属领导 | 李文静 |" in response.answer
        assert "| 事由 | 支持上海国网服务器部署 |" in response.answer
        assert "| 出行方式 | 飞机 |" in response.answer
        assert "| 系统预估费用 | 12000元 |" in response.answer
        assert "请核对上述信息无误" in response.answer
        assert "[确认](#application-submit)" in response.answer
        assert response.requires_confirmation is True
        assert response.suggested_actions == []


def test_user_agent_application_preview_uses_employee_grade_profile() -> None:
    session_factory = build_session_factory()
    initial_message = (
        "发生时间：2026-05-25\n"
        "地点：上海\n"
        "事由：支持上海国网服务器部署\n"
        "天数：3天"
    )
    with session_factory() as db:
        employee = Employee(
            employee_no="APP-GRADE-001",
            name="李文静",
            email="pytest-application-grade@example.com",
            position="解决方案顾问",
            grade="P5",
        )
        db.add(employee)
        db.commit()

        response = build_application_user_agent_response(
            db,
            "预计总费用：12000元",
            context_overrides={
                "name": "李文静",
                "manager_name": "王强",
            },
            history=[
                {"role": "user", "content": initial_message},
                {"role": "user", "content": "飞机"},
            ],
        )

        assert "这是费用申请核对结果" in response.answer
        assert "| 姓名 | 李文静 |" in response.answer
        assert "| 岗位 | 解决方案顾问 |" in response.answer
        assert "| 职级 | P5 |" in response.answer
        assert "| 职级 | 待补充 |" not in response.answer


def test_user_agent_application_submit_enters_leader_review() -> None:
    session_factory = build_session_factory()
    initial_message = (
        "发生时间：2026-05-25\n"
        "地点：上海\n"
        "事由：支持上海国网服务器部署\n"
        "天数：3天"
    )
    preview_answer = (
        "这是费用申请核对结果，请核对：\n"
        "| 字段 | 内容 |\n"
        "| --- | --- |\n"
        "| 申请类型 | 差旅费用申请 |\n"
        "| 出发时间 | 2026-05-25 |\n"
        "| 返回时间 | 2026-05-27 |\n"
        "| 地点 | 上海市 |\n"
        "| 事由 | 支持上海国网服务器部署 |\n"
        "| 天数 | 3天 |\n"
        "| 出行方式 | 飞机 |\n"
        "| 系统预估费用 | 12000元 |\n\n"
        "请核对上述信息无误，确认无误后 [确认](#application-submit) 提交至审批流程。"
    )
    with session_factory() as db:
        response = build_application_user_agent_response(
            db,
            "确认提交",
            context_overrides={"manager_name": "陈硕", "department_name": "技术部"},
            history=[
                {"role": "user", "content": initial_message},
                {"role": "user", "content": "飞机"},
                {"role": "user", "content": "预计总费用：12000元"},
                {"role": "assistant", "content": preview_answer},
            ],
        )

        assert "申请单据已生成，并已进入审批流程" in response.answer
        assert "系统已推送给 陈硕 审核，当前节点：陈硕审核中" in response.answer
        assert "下方是简要单据信息" in response.answer
        assert "申请信息：" not in response.answer
        assert re.search(r"A[A-HJ-NP-Z2-9]{8}", response.answer)
        assert response.suggested_actions == []
        claim = application_claim_query(db).one()
        assert claim.status == "submitted"
        assert claim.approval_stage == "直属领导审批"
        assert claim.expense_type == "travel_application"
        assert claim.amount == Decimal("12000.00")
        assert claim.employee_name == "pytest"


def test_user_agent_application_submit_blocks_duplicate_business_time() -> None:
    session_factory = build_session_factory()
    initial_message = (
        "出发时间：2026-05-25\n"
        "返回时间：2026-05-27\n"
        "地点：上海\n"
        "事由：支持上海国网服务器部署\n"
        "天数：3天\n"
        "出行方式：飞机\n"
        "预计总费用：12000元"
    )
    preview_answer = (
        "这是费用申请核对结果，请核对：\n"
        "| 字段 | 内容 |\n"
        "| --- | --- |\n"
        "| 申请类型 | 差旅费用申请 |\n"
        "| 出发时间 | 2026-05-25 |\n"
        "| 返回时间 | 2026-05-27 |\n"
        "| 地点 | 上海市 |\n"
        "| 事由 | 支持上海国网服务器部署 |\n"
        "| 天数 | 3天 |\n"
        "| 出行方式 | 飞机 |\n"
        "| 系统预估费用 | 12000元 |\n\n"
        "请核对上述信息无误，确认无误后 [确认](#application-submit) 提交至审批流程。"
    )
    history = [
        {"role": "user", "content": initial_message},
        {"role": "assistant", "content": preview_answer},
    ]
    with session_factory() as db:
        first_response = build_application_user_agent_response(
            db,
            "确认提交",
            context_overrides={"manager_name": "陈硕", "department_name": "技术部"},
            history=history,
        )
        first_claim = application_claim_query(db).one()

        second_response = build_application_user_agent_response(
            db,
            "确认提交",
            context_overrides={"manager_name": "陈硕", "department_name": "技术部"},
            history=history,
        )

        claims = application_claim_query(db).all()
        assert len(claims) == 1
        assert "申请单据已生成" in first_response.answer
        assert "已存在申请单" in second_response.answer
        assert "系统没有重复创建" in second_response.answer
        assert first_claim.claim_no in second_response.answer
        assert second_response.draft_payload is None


def test_user_agent_application_submit_blocks_overlapping_travel_dates() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        existing_claim = ExpenseClaim(
            id="application-overlap-1",
            claim_no="AP-202606050001-OVERLAP",
            employee_name="pytest",
            department_name="技术部",
            expense_type="travel_application",
            reason="支撑国网部署",
            location="北京",
            amount=Decimal("2700.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 6, 5, tzinfo=UTC),
            status="submitted",
            approval_stage="直属领导审批",
            risk_flags_json=[
                {
                    "source": "application_detail",
                    "business_stage": "expense_application",
                    "application_detail": {
                        "application_type": "差旅费用申请",
                        "time": "2026-06-05 至 2026-06-07",
                        "location": "北京",
                        "reason": "支撑国网部署",
                    },
                }
            ],
        )
        db.add(existing_claim)
        db.commit()

        response = build_application_user_agent_response(
            db,
            "确认提交",
            context_overrides={
                "manager_name": "向万红",
                "department_name": "技术部",
                "application_preview": {
                    "fields": {
                        "applicationType": "差旅费用申请",
                        "time": "2026-06-06 至 2026-06-08",
                        "location": "北京",
                        "reason": "支撑国网仿生产部署",
                        "days": "3天",
                        "transportMode": "火车",
                        "amount": "2700元",
                    }
                },
            },
        )

        claims = application_claim_query(db).all()
        assert len(claims) == 1
        assert "已存在申请单" in response.answer
        assert "系统没有重复创建" in response.answer
        assert existing_claim.claim_no in response.answer
        assert response.draft_payload is None


def test_user_agent_application_submit_normalizes_location_mixed_with_business_content() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        response = build_application_user_agent_response(
            db,
            "确认提交",
            context_overrides={
                "manager_name": "向万红",
                "department_name": "技术部",
                "application_preview": {
                    "fields": {
                        "applicationType": "差旅费用申请",
                        "time": "2026-02-20 至 2026-02-23",
                        "location": "上海辅助国网仿生产服务器",
                        "reason": "辅助国网仿生产服务器部署",
                        "days": "4天",
                        "transportMode": "火车",
                        "amount": "2120元",
                    }
                },
            },
        )

        claim = application_claim_query(db).one()
        assert claim.location == "上海市"
        assert claim.reason == "辅助国网仿生产服务器部署"
        assert "申请单据已生成" in response.answer
        assert response.draft_payload is not None


def test_user_agent_application_submit_splits_location_and_reason_from_raw_sentence() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        response = build_application_user_agent_response(
            db,
            "确认提交",
            history=[
                {
                    "role": "user",
                    "content": "2026-02-20 至 2026-02-23，去上海辅助国网仿生产服务器部署，火车",
                }
            ],
            context_overrides={
                "manager_name": "向万红",
                "grade": "P5",
                "department_name": "技术部",
            },
        )

        claim = application_claim_query(db).one()
        assert claim.location == "上海市"
        assert claim.reason == "辅助国网仿生产服务器部署"
        assert "申请单据已生成" in response.answer


def test_application_sentence_jieba_tokenizer_recognizes_location_boundary() -> None:
    tokens = resolve_jieba_tokens("上海辅助国网仿生产服务器部署")

    assert ("上海", "ns") in tokens
    assert [word for word, _ in tokens] == ["上海", "辅助", "国网", "仿生产", "服务器", "部署"]


def test_user_agent_application_maps_preview_travel_type_label() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        response = build_application_user_agent_response(
            db,
            "申请出差，2月20-23日上海，火车",
            context_overrides={
                "name": "曹笑竹",
                "department_name": "技术部",
                "grade": "P5",
                "application_preview": {
                    "fields": {
                        "applicationType": "travel",
                        "time": "2026-02-20 至 2026-02-23",
                        "location": "上海市",
                        "reason": "",
                        "days": "4天",
                        "transportMode": "火车",
                    }
                },
            },
        )

        assert "| 申请类型 | 差旅费用申请 |" in response.answer
        assert "| 申请类型 | travel |" not in response.answer


def test_user_agent_application_edit_resubmits_returned_application_claim() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        claim = ExpenseClaim(
            id="application-edit-1",
            claim_no="AP-20260220-EDIT",
            employee_name="pytest",
            department_name="技术部",
            expense_type="travel_application",
            reason="旧事由",
            location="上海",
            amount=Decimal("1000.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 2, 20, tzinfo=UTC),
            status="returned",
            approval_stage="待提交",
            risk_flags_json=[
                {
                    "source": "manual_return",
                    "event_type": "expense_application_return",
                    "message": "请修改事由",
                },
                {
                    "source": "application_detail",
                    "application_detail": {
                        "reason": "旧事由",
                        "time": "2026-02-20 至 2026-02-23",
                    },
                },
            ],
        )
        db.add(claim)
        db.commit()

        response = build_application_user_agent_response(
            db,
            "确认提交",
            context_overrides={
                "manager_name": "向万红",
                "application_edit_mode": True,
                "application_edit_claim_id": claim.id,
                "application_preview": {
                    "fields": {
                        "applicationType": "差旅费用申请",
                        "time": "2026-02-20 至 2026-02-23",
                        "location": "上海市",
                        "reason": "支撑国网仿生产环境建设",
                        "days": "4天",
                        "transportMode": "火车",
                        "amount": "4660元",
                        "grade": "P5",
                        "department": "技术部",
                        "position": "财务智能化产品经理",
                        "managerName": "向万红",
                    }
                },
            },
        )

        db.refresh(claim)
        claims = application_claim_query(db).all()
        assert len(claims) == 1
        assert "申请单据已修改并重新提交" in response.answer
        assert response.draft_payload is not None
        assert response.draft_payload.claim_id == claim.id
        assert claim.status == "submitted"
        assert claim.approval_stage == "直属领导审批"
        assert claim.reason == "支撑国网仿生产环境建设"
        assert claim.location == "上海市"
        assert claim.amount == Decimal("4660.00")
        assert claim.occurred_at.date().isoformat() == "2026-02-20"

        flags = list(claim.risk_flags_json or [])
        assert any(flag.get("event_type") == "expense_application_return" for flag in flags)
        assert any(flag.get("event_type") == "expense_application_submission" for flag in flags)
        detail_flags = [
            flag.get("application_detail")
            for flag in flags
            if isinstance(flag, dict) and flag.get("source") == "application_detail"
        ]
        assert len(detail_flags) == 1
        assert detail_flags[0]["reason"] == "支撑国网仿生产环境建设"
        assert detail_flags[0]["transport_mode"] == "火车"


def test_user_agent_knowledge_fallback_is_honest_and_personalized() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="住宿费标准是多少？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)
        answer = service._build_knowledge_search_answer(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="住宿费标准是多少？",
                ontology=ontology,
                context_json={"name": "张三"},
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [{"title": "差旅费制度", "content": "住宿费标准正文"}],
                },
            ),
            citations=[],
        )

        assert answer.startswith("张三，您好。")
        assert "## 结论" in answer
        assert "## 依据" in answer
        assert "## 说明" in answer
        assert "我先根据当前制度依据给出可以确认的部分" in answer
        assert "已命中" not in answer
        assert "答案整理阶段本轮没有及时返回" not in answer
        assert "《差旅费制度》" in answer


def test_user_agent_knowledge_answer_generation_uses_fast_timeouts(monkeypatch) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="住宿费标准是多少？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)
        captured: dict[str, object] = {}

        def fake_complete(messages, **kwargs):
            captured["messages"] = messages
            captured.update(kwargs)
            return "测试回答"

        monkeypatch.setattr(service.runtime_chat_service, "complete", fake_complete)

        answer = service._generate_answer_with_model(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="住宿费标准是多少？",
                ontology=ontology,
                tool_payload={"result_type": "knowledge_search", "hits": []},
            ),
            citations=[],
            suggested_actions=[],
            risk_flags=[],
            draft_payload=None,
            fallback_answer="",
        )

        assert answer == "测试回答"
        assert captured["timeout_seconds"] == 30
        assert captured["slot_timeouts"] == {"main": 20, "backup": 30}
        assert captured["max_attempts"] == 1


def test_user_agent_prefers_structured_table_hit_for_standard_query() -> None:
    selected = UserAgentService._select_knowledge_model_hits(
        {
            "hits": [
                {"content": "raw hit 1"},
                {"content": "raw hit 2"},
                {"content": "# 问答线索补充\n\n- 第二章 报销时限：费用发生后 30 日内提交申请。"},
                {"content": "# 结构化表格补充\n\n| 项目 | 餐补 |\n| 其他地区 | 55 |"},
            ]
        },
        question="餐补标准是多少？",
    )

    assert selected[0]["content"].startswith("# 结构化表格补充")
    assert any(item["content"].startswith("# 结构化表格补充") for item in selected[:2])


def test_user_agent_prefers_relevant_raw_hit_over_generic_appendix() -> None:
    selected = UserAgentService._select_knowledge_model_hits(
        {
            "hits": [
                {"content": "# 章节导航\n\n- 第一章 总则\n- 第二章 职责分工"},
                {"content": "# 问答线索补充\n\n- 第二章 职责分工：计划财务部负责财务审核。"},
                {"content": "一般性说明文字，没有探亲差旅归口信息。"},
                {"content": "附表3：支出归口管理部门与归口业务范围\n组织人事部：探亲差旅、条件艰苦及安全风险较高区域补助等支出。"},
            ]
        },
        question="探亲差旅归哪个部门管理？",
    )

    assert "组织人事部" in selected[0]["content"]


def test_user_agent_model_hit_selection_keeps_later_relevant_hits() -> None:
    selected = UserAgentService._select_knowledge_model_hits(
        {
            "hits": [
                {"content": "一般说明一"},
                {"content": "一般说明二"},
                {"content": "一般说明三"},
                {"content": "一般说明四"},
                {"content": "一般说明五"},
                {"content": "一般说明六"},
                {"content": "一般说明七"},
                {
                    "content": (
                        "# 问答线索补充\n\n"
                        "- 第二章 报销时限：差旅费应在行程结束三个月内提交；逾期不予报销出差补贴。"
                    )
                },
            ]
        },
        question="差旅费报销时限是多少？",
    )

    assert "三个月内提交" in selected[0]["content"]


def test_user_agent_knowledge_terms_keep_accounting_subject_in_long_query() -> None:
    terms = UserAgentService._extract_knowledge_query_terms(
        "远光软件财务基础知识手册里的常用会计科目是什么？"
    )

    assert "常用会计科目" in terms
    assert "会计科目" in terms


def test_user_agent_knowledge_answer_uses_model_after_retrieval(monkeypatch) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="报销时限是多少？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)
        captured: dict[str, object] = {}

        def fake_generate_answer(payload, **kwargs):
            captured["payload"] = payload
            captured.update(kwargs)
            return "## 结论\n\n员工应在费用发生后 30 日内提交报销申请。"

        monkeypatch.setattr(service, "_generate_answer_with_model", fake_generate_answer)

        response = service.respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="报销时限是多少？",
                ontology=ontology,
                context_json={
                    "name": "张三",
                    "session_type": "knowledge",
                    "user_input_text": "报销时限是多少？",
                },
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [
                        {
                            "title": "费用报销制度",
                            "content": (
                                "# 问答线索补充\n\n"
                                "- 第二章 报销时限：员工应在费用发生后 30 日内提交报销申请。\n"
                                "- 第二章 报销时限：超过 30 日需补充审批说明。"
                            ),
                        },
                    ],
                },
            )
        )

        assert captured["payload"].ontology.scenario == "knowledge"
        assert "费用报销制度" in captured["fallback_answer"]
        assert "## 依据" in captured["fallback_answer"]
        assert response.answer.startswith("## 结论")
        assert "30 日内提交报销申请" in response.answer
        assert "答案整理阶段本轮没有及时返回" not in response.answer


def test_user_agent_fast_knowledge_answer_focuses_inline_section_items() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="主要税种介绍",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)

        answer = service._build_fast_knowledge_answer(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="主要税种介绍",
                ontology=ontology,
                context_json={
                    "session_type": "knowledge",
                    "user_input_text": "主要税种介绍",
                },
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [
                        {
                            "title": "财务基础知识",
                            "content": (
                                "资产类 银行存款 企业存放在银行的款项 负债类 应付账款 "
                                "因购买商品或接受劳务应付的款项 负债类 应交税费 应缴纳的各种税费 "
                                "第二部分 税务基础知识 三、主要税种介绍 "
                                "（一）增值税：公司为一般纳税人，软件服务适用6%税率，软件产品销售适用13%税率。 "
                                "（二）企业所得税：税率为25%，高新技术企业享受15%优惠税率。 "
                                "（三）个人所得税：员工工资薪金由公司代扣代缴。 "
                                "（四）印花税：购销合同、账簿等按规定缴纳。"
                            ),
                        }
                    ],
                },
            ),
            citations=[],
        )

        assert answer is not None
        assert "主要税种介绍包括：增值税、企业所得税、个人所得税、印花税" in answer
        assert "软件服务适用6%税率" in answer
        assert "软件产品销售适用13%税率" in answer
        assert "高新技术企业享受15%优惠税率" in answer
        assert "员工工资薪金由公司代扣代缴" in answer
        assert "购销合同、账簿等按规定缴纳" in answer
        assert "应付账款" not in answer
        assert "银行存款" not in answer


def test_user_agent_fast_knowledge_answer_summarizes_financial_statements() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="三大财务报表 是什么？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)

        answer = service._build_fast_knowledge_answer(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="三大财务报表 是什么？",
                ontology=ontology,
                context_json={
                    "session_type": "knowledge",
                    "user_input_text": "三大财务报表 是什么？",
                },
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [
                        {
                            "title": "财务基础知识",
                            "content": (
                                "第三部分 财务报表解读 四、三大财务报表 "
                                "（一）资产负债表：反映企业在某一特定日期的财务状况。 "
                                "（二）利润表：反映企业在一定期间的经营成果。 "
                                "（三）现金流量表：反映企业在一定期间现金和现金等价物的流入和流出。"
                            ),
                        }
                    ],
                },
            ),
            citations=[],
        )

        assert answer is not None
        assert "三大财务报表包括：资产负债表、利润表、现金流量表" in answer
        assert "资产负债表：反映企业在某一特定日期的财务状况" in answer
        assert "利润表：反映企业在一定期间的经营成果" in answer
        assert "现金流量表：反映企业在一定期间现金和现金等价物的流入和流出" in answer
        assert "第三部分 财务报表解读" not in answer


def test_user_agent_fast_knowledge_answer_expands_broad_accounting_table() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="常用会计科目是什么？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)

        answer = service._build_fast_knowledge_answer(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="常用会计科目是什么？",
                ontology=ontology,
                context_json={
                    "session_type": "knowledge",
                    "user_input_text": "常用会计科目是什么？",
                },
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [
                        {
                            "title": "财务基础知识",
                            "content": (
                                "二、常用会计科目\n\n"
                                "| 科目类别 | 科目名称 | 说明 |\n"
                                "| --- | --- | --- |\n"
                                "| 资产类 | 库存现金 | 公司持有的现金 |\n"
                                "| 资产类 | 银行存款 | 存放在银行的资金 |\n"
                                "| 资产类 | 应收账款 | 因销售商品或提供劳务应收的款项 |\n"
                                "| 资产类 | 固定资产 | 使用年限超过一年的有形资产 |\n"
                                "| 负债类 | 应付账款 | 因购买商品或接受劳务应付的款项 |\n"
                                "| 负债类 | 应交税费 | 应缴纳的各种税费 |\n"
                                "| 负债类 | 应付职工薪酬 | 应付给职工的工资、福利等 |\n"
                                "| 损益类 | 主营业务收入 | 主要经营业务产生的收入 |\n"
                                "| 损益类 | 管理费用 | 为管理生产经营发生的费用 |\n"
                                "| 损益类 | 销售费用 | 为销售产品发生的费用 |\n"
                            ),
                        }
                    ],
                },
            ),
            citations=[],
        )

        assert answer is not None
        assert "| 科目类别 | 科目名称 | 说明 |" in answer
        assert "| 资产类 | 库存现金 | 公司持有的现金 |" in answer
        assert "| 负债类 | 应付职工薪酬 | 应付给职工的工资、福利等 |" in answer
        assert "| 损益类 | 销售费用 | 为销售产品发生的费用 |" in answer


def test_user_agent_fast_knowledge_answer_renders_relevant_table_preview() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="餐补标准是多少？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)

        answer = service._build_fast_knowledge_answer(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="餐补标准是多少？",
                ontology=ontology,
                context_json={
                    "session_type": "knowledge",
                    "user_input_text": "餐补标准是多少？",
                },
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [
                        {
                            "title": "费用报销制度",
                            "content": (
                                "# 结构化表格补充\n\n"
                                "## 表3 出差补贴标准\n\n"
                                "| 项目 | 港澳台 | 其他地区 | 国外 |\n"
                                "| --- | --- | --- | --- |\n"
                                "| 餐补 | 75 | 55 | 140 |\n"
                                "| 住宿补贴 | 35 | 35 | 35 |\n"
                            ),
                        }
                    ],
                },
            ),
            citations=[],
        )

        assert answer is not None
        assert "| 项目 | 港澳台 | 其他地区 | 国外 |" in answer
        assert "| 餐补 | 75 | 55 | 140 |" in answer
        assert "餐补的标准为" in answer
        assert "## 结论" in answer
        assert "## 依据" in answer


def test_user_agent_fast_knowledge_answer_uses_user_grade_for_table_row() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我的住宿费标准是多少？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)

        answer = service._build_fast_knowledge_answer(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我的住宿费标准是多少？",
                ontology=ontology,
                context_json={
                    "name": "张三",
                    "grade": "P5",
                    "position": "实施经理",
                    "session_type": "knowledge",
                    "user_input_text": "我的住宿费标准是多少？",
                },
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [
                        {
                            "title": "费用报销制度",
                            "content": (
                                "# 结构化表格补充\n\n"
                                "## 国内住宿限额标准\n\n"
                                "| 职级 | 直辖市/特区/港澳台 | 省会城市 | 其他地区 |\n"
                                "| --- | --- | --- | --- |\n"
                                "| 公司领导（P8及以上） | 800 | 500 | 400 |\n"
                                "| 高层经理（P7） | 700 | 450 | 400 |\n"
                                "| 中层经理、基层经理（P4～P6、外聘专家） | 600 | 400 | 350 |\n"
                                "| 其他员工 | 500 | 350 | 300 |\n"
                            ),
                        }
                    ],
                },
            ),
            citations=[],
        )

        assert answer is not None
        assert answer.startswith("张三，您好。")
        assert "中层经理、基层经理（P4～P6、外聘专家）的标准为" in answer
        assert "| 中层经理、基层经理（P4～P6、外聘专家） | 600 | 400 | 350 |" in answer
        assert "| 公司领导（P8及以上） | 800 | 500 | 400 |" not in answer
        assert "| 高层经理（P7） | 700 | 450 | 400 |" not in answer


def test_user_agent_fast_knowledge_answer_notes_missing_location_grounding() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="前往北京出差的报销标准是什么？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)

        answer = service._build_fast_knowledge_answer(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="前往北京出差的报销标准是什么？",
                ontology=ontology,
                context_json={
                    "session_type": "knowledge",
                    "user_input_text": "前往北京出差的报销标准是什么？",
                },
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [
                        {
                            "title": "费用报销制度",
                            "content": (
                                "# 结构化表格补充\n\n"
                                "## 表3 出差补贴标准\n\n"
                                "| 项目 | 港澳台 | 直辖市/特区/西藏 | 其他地区 |\n"
                                "| --- | --- | --- | --- |\n"
                                "| 餐补 | 75 | 65 | 55 |\n"
                                "| 基本补贴 | 35 | 35 | 35 |\n"
                            ),
                        }
                    ],
                },
            ),
            citations=[],
        )

        assert answer is not None
        assert "没有直接写出“北京”对应的地区档位或映射关系" in answer
        assert "## 说明" in answer
        assert "## 依据" in answer


def test_user_agent_fast_knowledge_answer_expands_lead_in_list_items() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="出差记录链条中断时，要提供哪些业务佐证材料？",
                user_id="pytest",
                context_json={"session_type": "knowledge"},
            )
        )
        service = UserAgentService(db)

        answer = service._build_fast_knowledge_answer(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="出差记录链条中断时，要提供哪些业务佐证材料？",
                ontology=ontology,
                context_json={
                    "session_type": "knowledge",
                    "user_input_text": "出差记录链条中断时，要提供哪些业务佐证材料？",
                },
                tool_payload={
                    "result_type": "knowledge_search",
                    "hits": [
                        {
                            "title": "费用报销制度",
                            "content": (
                                "第十三条 差旅费\n\n"
                                "（2）出差记录链条中断时，应提供业务佐证材料：\n"
                                "① 登机牌、高速道路通行记录、其他道路通行记录、租车记录等。\n"
                                "② 支付记录。\n"
                                "③ 出差审批邮件、短信、微信等。"
                            ),
                        }
                    ],
                },
            ),
            citations=[],
        )

        assert answer is not None
        assert "## 结论" in answer
        assert "登机牌、高速道路通行记录" in answer
        assert "支付记录" in answer
        assert "出差审批邮件、短信、微信等" in answer
        assert "（3）" not in answer
        assert "## 依据" in answer


def test_user_agent_model_prompt_supports_contextual_personalization() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我能坐什么舱位？",
                user_id="pytest",
            )
        )
        service = UserAgentService(db)
        messages = service._build_model_messages(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我能坐什么舱位？",
                ontology=ontology,
                context_json={
                    "name": "张三",
                    "position": "财务分析师",
                    "grade": "P5",
                    "role": "财务人员",
                    "role_codes": ["finance"],
                },
                tool_payload={},
            ),
            citations=[],
            suggested_actions=[],
            risk_flags=[],
            draft_payload=None,
            fallback_answer="",
        )

        system_prompt = messages[0]["content"]
        user_prompt = messages[1]["content"]
        assert "context.user_grade" in system_prompt
        assert "conversation_history" in user_prompt
        assert '"user_name": "张三"' in user_prompt
        assert '"user_position": "财务分析师"' in user_prompt
        assert '"user_grade": "P5"' in user_prompt


def test_user_agent_guides_generic_expense_request() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我要报销",
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我要报销",
                ontology=ontology,
                tool_payload={"record_count": 9, "total_amount": 12345.0},
            )
        )

        assert response.review_payload is None
        assert response.draft_payload is None
        assert "请先在下面选择报销场景" in response.answer
        assert [item.action_type for item in response.suggested_actions] == [
            "select_expense_type",
            "select_expense_type",
            "select_expense_type",
            "select_expense_type",
            "select_expense_type",
            "select_expense_type",
        ]
        assert [item.label for item in response.suggested_actions[:3]] == ["差旅费", "交通费", "住宿费"]


def test_user_agent_asks_for_type_when_trip_context_is_ambiguous() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        message = "业务发生时间:2026-02-20 至 2026-02-23，去上海支持上海电力部署项目，申请报销"
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest-ambiguous-type@example.com",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-ambiguous-type@example.com",
                message=message,
                ontology=ontology,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is None
        assert response.draft_payload is None
        assert "请先在下面选择报销场景" in response.answer
        assert "避免系统先入为主" in response.answer
        assert [item.label for item in response.suggested_actions] == [
            "差旅费",
            "交通费",
            "住宿费",
            "业务招待费",
            "会务费",
            "办公用品费",
            "培训费",
            "通讯费",
            "福利费",
            "其他费用",
        ]
        assert response.suggested_actions[0].payload["original_message"] == message


def test_user_agent_continues_identification_after_expense_type_selection() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        message = "业务发生时间:2026-02-20 至 2026-02-23，去上海支持上海电力部署项目，申请报销"
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=f"{message}\n用户选择报销场景：差旅费",
                user_id="pytest-selected-type@example.com",
                context_json={
                    "grade": "P4",
                    "expense_scene_selection": {
                        "expense_type": "travel",
                        "expense_type_label": "差旅费",
                        "original_message": message,
                    },
                    "review_form_values": {
                        "expense_type": "差旅费",
                    },
                    "user_input_text": message,
                },
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-selected-type@example.com",
                message=f"{message}\n用户选择报销场景：差旅费",
                ontology=ontology,
                context_json={
                    "grade": "P4",
                    "expense_scene_selection": {
                        "expense_type": "travel",
                        "expense_type_label": "差旅费",
                        "original_message": message,
                    },
                    "review_form_values": {
                        "expense_type": "差旅费",
                    },
                    "user_input_text": message,
                },
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["expense_type"].value == "差旅费"
        assert slot_map["expense_type"].normalized_value == "travel"
        assert slot_map["time_range"].value == "2026-02-20 至 2026-02-23"
        assert slot_map["location"].value == "上海"
        assert "报销测算参考：" in response.answer
        assert "| 项目 | 测算口径 | 金额 |" in response.answer
        assert "| 住宿标准 |" in response.answer
        assert "| 出差补贴 |" in response.answer
        assert "| 参考合计 |" in response.answer


def test_user_agent_uses_linked_application_context_for_review_slots() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        message = "请生成差旅费报销草稿"
        context_json = {
            "grade": "P4",
            "expense_scene_selection": {
                "expense_type": "travel",
                "expense_type_label": "差旅费",
                "original_message": message,
                "application_claim_id": "application-linked-1",
                "application_claim_no": "AP-202606-001",
            },
            "review_form_values": {
                "expense_type": "差旅费",
                "application_claim_id": "application-linked-1",
                "application_claim_no": "AP-202606-001",
                "application_reason": "支撑国网仿生产环境部署",
                "application_location": "北京",
                "application_amount": "3000元",
                "application_business_time": "2026-06-01 至 2026-06-03",
                "application_transport_mode": "火车",
            },
            "user_input_text": message,
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest-linked-application-review@example.com",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-linked-application-review@example.com",
                message=message,
                ontology=ontology,
                context_json=context_json,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["reason"].value == "支撑国网仿生产环境部署"
        assert slot_map["location"].value == "北京"
        assert slot_map["amount"].value == "3000.00元"
        assert slot_map["time_range"].value == "2026-06-01 至 2026-06-03"
        assert UserAgentService._resolve_review_form_values(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-linked-application-review@example.com",
                message=message,
                ontology=ontology,
                context_json=context_json,
            )
        )["transport_mode"] == "火车"
        assert "事由说明" not in response.review_payload.missing_slots


def test_user_agent_guides_implicit_expense_draft_request() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        today = datetime.now(UTC).date().isoformat()
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我今天去客户现场，招待了客户，花销了1000元",
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我今天去客户现场，招待了客户，花销了1000元",
                ontology=ontology,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        assert response.answer == response.review_payload.body_message
        assert response.review_payload.intent_summary.startswith("识别到您希望报销一笔“业务招待费”费用：")
        assert "基础信息识别结果：" in response.review_payload.intent_summary
        assert response.review_payload.missing_slots == ["客户名称", "参与人员", "票据附件"]
        assert [item.action_type for item in response.review_payload.confirmation_actions] == [
            "save_draft",
        ]

        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["expense_type"].value == "业务招待费"
        assert slot_map["time_range"].value == today
        assert slot_map["time_range"].raw_value == "今天"
        assert slot_map["location"].value == "客户现场"
        assert slot_map["amount"].value == "1000.00元"


def test_user_agent_guides_narrative_with_day_before_yesterday() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我前天请客户吃饭花了200元",
                user_id="pytest",
                context_json={
                    "client_now_iso": "2026-05-12T16:30:00.000Z",
                    "client_timezone_offset_minutes": -480,
                },
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我前天请客户吃饭花了200元",
                ontology=ontology,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["time_range"].raw_value == "前天"
        assert slot_map["time_range"].value == "2026-05-11"
        assert "时间：2026-05-11" in response.review_payload.intent_summary


def test_user_agent_guides_riding_fare_as_transport_expense() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        message = "业务发生时间:2026-03-04，送客户去林萃小区办事，请报销乘车费用"
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=message,
                ontology=ontology,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["expense_type"].value == "交通费"
        assert slot_map["expense_type"].normalized_value == "transport"
        assert slot_map["time_range"].value == "2026-03-04"
        assert slot_map["reason"].value == "送客户去林萃小区办事，请报销乘车费用"
        assert "业务发生时间" not in slot_map["reason"].raw_value
        assert "“交通费”" in response.review_payload.intent_summary


def test_user_agent_keeps_taxi_ticket_for_customer_dropoff_as_transport_expense() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        message = "送客户去机场，报销的士票"
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=message,
                ontology=ontology,
                tool_payload={"draft_only": True},
            )
        )

        assert ontology.intent == "draft"
        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["expense_type"].value == "交通费"
        assert slot_map["expense_type"].normalized_value == "transport"
        assert slot_map["reason"].value == "送客户去机场，报销的士票"
        assert "业务招待费" not in response.review_payload.intent_summary
        assert "客户名称" not in response.review_payload.missing_slots
        assert "参与人员" not in response.review_payload.missing_slots
        edit_field_keys = {item.key for item in response.review_payload.edit_fields}
        assert "merchant_name" not in edit_field_keys
        assert "participants" not in edit_field_keys


def test_user_agent_keeps_travel_range_when_user_adds_receipts_after_text_context() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        message = "业务发生时间:2026-02-20 至 2026-02-23，去上海支撑上海电力 服务器部署，出差3天"
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest-travel-range@example.com",
            )
        )
        initial_response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-travel-range@example.com",
                message=message,
                ontology=ontology,
                tool_payload={"draft_only": True},
            )
        )

        assert initial_response.review_payload is not None
        initial_slots = {item.key: item for item in initial_response.review_payload.slot_cards}
        assert initial_slots["expense_type"].normalized_value == "travel"
        assert initial_slots["time_range"].value == "2026-02-20 至 2026-02-23"
        assert initial_slots["location"].value == "上海"
        assert "业务发生时间" not in initial_slots["reason"].raw_value
        assert not initial_slots["reason"].value.startswith("至 2026-02-23")

        followup_context = {
            "name": "张三",
            "grade": "P4",
            "review_action": "link_to_existing_draft",
            "review_form_values": {
                "expense_type": "差旅费",
                "occurred_date": "2026-02-20",
                "time_range": "2026-02-20 至 2026-02-23",
                "business_time": "2026-02-20 至 2026-02-23",
                "business_location": "上海",
                "reason": "去上海支撑上海电力服务器部署，出差3天",
            },
            "business_time_context": {
                "mode": "range",
                "start_date": "2026-02-20",
                "end_date": "2026-02-23",
                "display_value": "2026-02-20 至 2026-02-23",
            },
            "attachment_names": ["2月20_武汉-上海.pdf", "2月23_上海-武汉.pdf", "上海酒店发票.pdf"],
            "attachment_count": 3,
            "ocr_documents": [
                {
                    "filename": "2月20_武汉-上海.pdf",
                    "document_type": "train_ticket",
                    "scene_code": "travel",
                    "scene_label": "差旅票据",
                    "summary": "铁路电子客票 2026-02-20 武汉-上海 二等座 票价 354 元",
                    "text": "铁路电子客票 2026-02-20 武汉-上海 二等座 票价 ￥354.00",
                    "avg_score": 0.95,
                    "document_fields": [
                        {"key": "amount", "label": "票价", "value": "354"},
                        {"key": "route", "label": "行程", "value": "武汉-上海"},
                        {"key": "date", "label": "日期", "value": "2026-02-20"},
                    ],
                    "warnings": [],
                },
                {
                    "filename": "2月23_上海-武汉.pdf",
                    "document_type": "train_ticket",
                    "scene_code": "travel",
                    "scene_label": "差旅票据",
                    "summary": "铁路电子客票 2026-02-23 上海-武汉 二等座 票价 354 元",
                    "text": "铁路电子客票 2026-02-23 上海-武汉 二等座 票价 ￥354.00",
                    "avg_score": 0.95,
                    "document_fields": [
                        {"key": "amount", "label": "票价", "value": "354"},
                        {"key": "route", "label": "行程", "value": "上海-武汉"},
                        {"key": "date", "label": "日期", "value": "2026-02-23"},
                    ],
                    "warnings": [],
                },
                {
                    "filename": "上海酒店发票.pdf",
                    "document_type": "hotel_invoice",
                    "summary": "上海酒店 住宿 3 晚 金额 1200 元",
                    "text": "上海酒店 住宿 3 晚 金额 1200 元",
                    "avg_score": 0.96,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "1200"},
                        {"key": "merchant", "label": "酒店名称", "value": "上海酒店"},
                    ],
                    "warnings": [],
                },
            ],
        }
        followup_ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="请把当前上传的票据合并到现有报销草稿中。",
                user_id="pytest-travel-range@example.com",
                context_json=followup_context,
            )
        )
        followup_response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=followup_ontology.run_id,
                user_id="pytest-travel-range@example.com",
                message="请把当前上传的票据合并到现有报销草稿中。",
                ontology=followup_ontology,
                context_json=followup_context,
                tool_payload={"draft_only": True},
            )
        )

        assert followup_response.review_payload is not None
        followup_slots = {item.key: item for item in followup_response.review_payload.slot_cards}
        assert followup_slots["expense_type"].value == "差旅费"
        assert followup_slots["expense_type"].normalized_value == "travel"
        assert followup_slots["time_range"].value == "2026-02-20 至 2026-02-23"
        assert followup_slots["location"].value == "上海"
        assert followup_slots["reason"].value == "去上海支撑上海电力服务器部署，出差3天"
        followup_risk_text = "\n".join(
            f"{item.title}\n{item.content}\n{item.detail}"
            for item in followup_response.review_payload.risk_briefs
        )
        assert "票据城市与申报目的地不一致" not in followup_risk_text
        assert "差旅目的地与票据城市不一致" not in followup_risk_text


def test_user_agent_does_not_treat_draft_saved_message_as_precheck_risk_for_transport() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        message = "业务发生时间:2026-03-04，送客户去林萃小区办事，打车花了32元，请报销乘车费用"
        context_json = {
            "name": "赵六",
            "attachment_names": ["didi-trip.png"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "didi-trip.png",
                    "summary": "滴滴出行 支付金额 32 元",
                    "text": "滴滴出行 支付金额 32 元",
                    "document_type": "taxi_receipt",
                    "scene_code": "transport",
                    "document_fields": [
                        {"key": "amount", "label": "支付金额", "value": "32.00"},
                    ],
                    "warnings": [],
                }
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=message,
                user_id="pytest",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=message,
                ontology=ontology,
                context_json=context_json,
                tool_payload={
                    "draft_only": True,
                    "claim_id": "claim-1",
                    "claim_no": "EXP-202603-001",
                    "status": "draft",
                    "message": (
                        "已创建报销草稿 EXP-202603-001，当前状态为 draft。"
                        "你可以继续补充费用明细、客户单位和票据附件。"
                    ),
                },
            )
        )

        assert response.review_payload is not None
        assert response.review_payload.can_proceed is True
        assert "客户名称" not in response.review_payload.missing_slots
        assert "参与人员" not in response.review_payload.missing_slots
        assert "票据附件" not in response.review_payload.missing_slots
        risk_text = "\n".join(
            f"{item.title}\n{item.content}" for item in response.review_payload.risk_briefs
        )
        assert "AI预审未通过" not in risk_text
        assert "已创建报销草稿" not in risk_text


def test_user_agent_attachment_only_upload_uses_generic_scene_reason_without_fabrication() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我上传了 1 份票据，请结合附件名称给出报销建议并尽量生成草稿。",
                user_id="pytest",
                context_json={
                    "attachment_names": ["didi-trip.png"],
                    "attachment_count": 1,
                    "ocr_documents": [
                        {
                            "filename": "didi-trip.png",
                            "summary": "滴滴出行 订单金额 32 元",
                            "text": "滴滴出行 订单金额 32 元",
                            "document_type": "taxi_receipt",
                            "scene_code": "transport",
                        }
                    ],
                    "user_input_text": "",
                },
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我上传了 1 份票据，请结合附件名称给出报销建议并尽量生成草稿。\n附件名称：didi-trip.png",
                ontology=ontology,
                context_json={
                    "attachment_names": ["didi-trip.png"],
                    "attachment_count": 1,
                    "ocr_documents": [
                        {
                            "filename": "didi-trip.png",
                            "summary": "滴滴出行 订单金额 32 元",
                            "text": "滴滴出行 订单金额 32 元",
                            "document_type": "taxi_receipt",
                            "scene_code": "transport",
                        }
                    ],
                    "user_input_text": "",
                },
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["reason"].value == "交通出行"
        assert slot_map["reason"].status == "inferred"


def test_user_agent_transport_flow_infers_reason_and_does_not_require_location_or_merchant() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我上传了交通票据，帮我生成报销草稿",
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我上传了交通票据，帮我生成报销草稿",
                ontology=ontology,
                context_json={
                    "attachment_names": ["didi-trip.png"],
                    "attachment_count": 1,
                    "ocr_documents": [
                        {
                            "filename": "didi-trip.png",
                            "summary": "滴滴出行 支付金额 32 元",
                            "text": "滴滴出行 支付金额 32 元",
                            "document_type": "taxi_receipt",
                            "scene_code": "transport",
                            "scene_label": "交通票据",
                        }
                    ],
                },
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        document_card = response.review_payload.document_cards[0]
        assert document_card.scene_label == "出租车/网约车票据"
        assert document_card.suggested_expense_type == "transport"
        assert slot_map["reason"].value == "交通出行"
        assert slot_map["reason"].status == "inferred"
        assert "酒店/商户" not in response.review_payload.missing_slots
        assert "地点" not in response.review_payload.missing_slots
        assert "事由说明" not in response.review_payload.missing_slots


def test_user_agent_risk_response_includes_rule_citations() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="检查重复报销风险",
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="检查重复报销风险",
                ontology=ontology,
                tool_payload={"risk_flags": ["duplicate_expense"]},
            )
        )

        assert response.risk_flags == ["duplicate_expense"]
        assert any(item.source_type == "rule" for item in response.citations)
        assert "duplicate_expense" in response.answer


def test_user_agent_draft_returns_structured_payload() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="帮我生成张三4月差旅报销草稿",
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="帮我生成张三4月差旅报销草稿",
                ontology=ontology,
                tool_payload={"draft_only": True},
            )
        )

        assert response.draft_payload is not None
        assert response.draft_payload.confirmation_required is True
        assert response.review_payload is not None
        assert response.review_payload.can_proceed is False
        assert response.review_payload.missing_slots == ["金额", "事由说明", "票据附件"]
        assert [item.action_type for item in response.review_payload.confirmation_actions] == [
            "save_draft",
        ]
        assert response.answer == response.review_payload.body_message


def test_user_agent_returns_draft_limit_message_when_save_is_blocked() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="请按当前识别信息保存报销草稿",
                user_id="pytest",
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="请按当前识别信息保存报销草稿",
                ontology=ontology,
                context_json={"review_action": "save_draft"},
                tool_payload={
                    "draft_limit_reached": True,
                    "message": "您当前已保存 3 个草稿，请先完成已保存的草稿，才能再次新建草稿。",
                    "status": "blocked",
                },
            )
        )

        assert (
            response.answer
            == "您当前已保存 3 个草稿，请先完成已保存的草稿，才能再次新建草稿。"
        )


def test_user_agent_save_draft_answer_guides_followup_to_existing_draft() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        context_json = {"review_action": "save_draft"}
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="请按当前识别信息保存报销草稿",
                user_id="pytest",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="请按当前识别信息保存报销草稿",
                ontology=ontology,
                context_json=context_json,
                tool_payload={
                    "claim_id": "claim-1",
                    "claim_no": "BX202605220001",
                    "status": "draft",
                    "approval_stage": "待提交",
                },
            )
        )

        assert response.draft_payload is not None
        assert response.draft_payload.claim_no == "BX202605220001"
        assert "已按您当前确认的信息保存为草稿 BX202605220001" in response.answer
        assert "系统已完成草稿规则校验" in response.answer
        assert "继续在当前对话上传" in response.answer
        assert "请关联这张草稿" not in response.answer
        assert "继续保存草稿" not in response.answer


def test_user_agent_returns_submitted_draft_payload_for_review_next_step() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        context_json = {
            "review_action": "next_step",
            "draft_claim_id": "claim-1",
            "attachment_count": 1,
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我已核对右侧识别结果，请进入下一步。",
                user_id="pytest",
                context_json=context_json,
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我已核对右侧识别结果，请进入下一步。",
                ontology=ontology,
                context_json=context_json,
                tool_payload={
                    "claim_id": "claim-1",
                    "claim_no": "BX202605200001",
                    "status": "submitted",
                    "approval_stage": "直属领导审批",
                },
            )
        )

        assert response.draft_payload is not None
        assert response.draft_payload.status == "submitted"
        assert response.draft_payload.confirmation_required is False
        assert "当前节点为 直属领导审批" in response.answer


def test_user_agent_document_service_normalizes_ocr_fields_and_scene() -> None:
    document_service = UserAgentDocumentService()

    fields = document_service.extract_document_fields(
        {
            "filename": "北京南站火车票.png",
            "document_type": "train_ticket",
            "scene_code": "travel",
            "summary": "电子发票 2026-03-04 广州南至北京南 二等座 票价 ￥560.00 中国铁路",
            "text": "电子发票 2026-03-04 广州南至北京南 二等座 票价 ￥560.00 中国铁路",
            "document_fields": [
                {"key": "amount", "label": "票价", "value": "￥560.00"},
                {"key": "date", "label": "业务发生时间", "value": "2026-03-04"},
                {"key": "merchant_name", "label": "商户", "value": "中国铁路"},
            ],
        }
    )
    classified = document_service.classify_document(
        {"filename": "客户餐饮发票.jpg", "summary": "餐饮发票 客户招待 金额 320 元"},
        expense_type_code="entertainment",
        has_customer=True,
    )

    assert fields["金额"] == "560.00元"
    assert fields["列车出发时间"] == "2026-03-04"
    assert "商户/酒店" not in fields
    assert document_service.extract_amount_text_from_value("滴滴出行 支付金额 1 元，实付 13.4 元，订单号 12345678") == "13.40元"
    taxi_classified = document_service.classify_document({"filename": "行程单_的士票.jpg", "summary": "的士 车费 48 元"})
    assert taxi_classified["document_type"] == "taxi_receipt"
    assert taxi_classified["expense_type"] == "transport"
    assert taxi_classified["scene_label"] == "出租车/网约车票据"
    ship_classified = document_service.classify_document({"filename": "轮船票.jpg", "summary": "轮船 船票 金额 180 元"})
    assert ship_classified["document_type"] == "ship_ticket"
    assert ship_classified["scene_label"] == "轮船票"
    assert classified["document_type"] == "meal_receipt"
    assert classified["expense_type"] == "meal"
    assert document_service.infer_expense_type_from_documents(
        [{"filename": "客户餐饮发票.jpg", "summary": "餐饮发票 客户招待 金额 320 元"}],
        expense_type_code="entertainment",
        has_customer=True,
    ) == "业务招待费"


def test_user_agent_builds_review_payload_for_multi_document_expense_flow() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        yesterday = (datetime.now(UTC).date() - timedelta(days=1)).isoformat()
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我昨天去上海出差，还请客户A吃饭，帮我生成报销草稿",
                user_id="pytest",
                context_json={
                    "attachment_names": ["机票行程单.png", "餐饮发票.jpg"],
                    "attachment_count": 2,
                    "ocr_documents": [
                        {
                            "filename": "机票行程单.png",
                            "summary": "机票行程单 上海-北京 金额 680 元",
                            "text": "机票行程单 上海-北京 金额 680 元",
                            "avg_score": 0.93,
                            "warnings": [],
                        },
                        {
                            "filename": "餐饮发票.jpg",
                            "summary": "餐饮发票 客户招待 金额 320 元",
                            "text": "餐饮发票 客户招待 金额 320 元",
                            "avg_score": 0.91,
                            "warnings": [],
                        },
                    ],
                },
            )
        )
        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我昨天去上海出差，还请客户A吃饭，帮我生成报销草稿",
                ontology=ontology,
                context_json={
                    "name": "张三",
                    "attachment_names": ["机票行程单.png", "餐饮发票.jpg"],
                    "attachment_count": 2,
                    "ocr_documents": [
                        {
                            "filename": "机票行程单.png",
                            "summary": "机票行程单 上海-北京 金额 680 元",
                            "text": "机票行程单 上海-北京 金额 680 元",
                            "avg_score": 0.93,
                            "warnings": [],
                        },
                        {
                            "filename": "餐饮发票.jpg",
                            "summary": "餐饮发票 客户招待 金额 320 元",
                            "text": "餐饮发票 客户招待 金额 320 元",
                            "avg_score": 0.91,
                            "warnings": [],
                        },
                    ],
                },
                tool_payload={"draft_only": True, "claim_no": "EXP-202605-009", "status": "draft"},
            )
        )

        assert response.review_payload is not None
        assert len(response.review_payload.document_cards) == 2
        assert len(response.review_payload.claim_groups) == 2
        assert response.review_payload.missing_slots == ["参与人员", "酒店的报销票据待上传（必须）"]
        assert [item.action_type for item in response.review_payload.confirmation_actions] == [
            "save_draft",
        ]
        assert any(item.scene_label == "餐饮发票" for item in response.review_payload.document_cards)
        assert all(item.scene_label != "业务招待费" for item in response.review_payload.document_cards)
        assert any(item.scene_label == "业务招待费" for item in response.review_payload.claim_groups)
        assert f"时间：{yesterday}" in response.review_payload.intent_summary
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["time_range"].value == yesterday
        assert slot_map["time_range"].raw_value == "昨天"


def test_user_agent_sums_multi_document_amounts_from_synonym_fields() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我上传了两张交通票据，帮我生成报销草稿",
                user_id="pytest",
                context_json={
                    "attachment_names": ["滴滴行程单.png", "停车票.jpg"],
                    "attachment_count": 2,
                    "ocr_documents": [
                        {
                            "filename": "滴滴行程单.png",
                            "summary": "滴滴出行电子行程单",
                            "text": "滴滴出行 订单金额 ￥32.50",
                            "avg_score": 0.94,
                            "document_fields": [
                                {"key": "amount", "label": "支付金额", "value": "32.50"},
                            ],
                            "warnings": [],
                        },
                        {
                            "filename": "停车票.jpg",
                            "summary": "停车票",
                            "text": "停车费 合计 18 元",
                            "avg_score": 0.92,
                            "document_fields": [
                                {"key": "total_amount", "label": "合计金额", "value": "18"},
                            ],
                            "warnings": [],
                        },
                    ],
                },
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我上传了两张交通票据，帮我生成报销草稿",
                ontology=ontology,
                context_json={
                    "attachment_names": ["滴滴行程单.png", "停车票.jpg"],
                    "attachment_count": 2,
                    "ocr_documents": [
                        {
                            "filename": "滴滴行程单.png",
                            "summary": "滴滴出行电子行程单",
                            "text": "滴滴出行 订单金额 ￥32.50",
                            "avg_score": 0.94,
                            "document_fields": [
                                {"key": "amount", "label": "支付金额", "value": "32.50"},
                            ],
                            "warnings": [],
                        },
                        {
                            "filename": "停车票.jpg",
                            "summary": "停车票",
                            "text": "停车费 合计 18 元",
                            "avg_score": 0.92,
                            "document_fields": [
                                {"key": "total_amount", "label": "合计金额", "value": "18"},
                            ],
                            "warnings": [],
                        },
                    ],
                },
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["amount"].value == "50.50元"
        document_field_labels = [
            field.label
            for card in response.review_payload.document_cards
            for field in card.fields
        ]
        assert "金额" in document_field_labels


def test_user_agent_prefers_larger_decimal_amount_from_ocr_text_candidates() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我上传了打车票据，帮我生成报销草稿",
                user_id="pytest",
                context_json={
                    "attachment_names": ["滴滴行程单.png"],
                    "attachment_count": 1,
                    "ocr_documents": [
                        {
                            "filename": "滴滴行程单.png",
                            "summary": "滴滴出行电子行程单",
                            "text": "滴滴出行 支付金额 1 元，实付 13.4 元，订单号 12345678",
                            "avg_score": 0.94,
                            "warnings": [],
                        },
                    ],
                },
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我上传了打车票据，帮我生成报销草稿",
                ontology=ontology,
                context_json={
                    "attachment_names": ["滴滴行程单.png"],
                    "attachment_count": 1,
                    "ocr_documents": [
                        {
                            "filename": "滴滴行程单.png",
                            "summary": "滴滴出行电子行程单",
                            "text": "滴滴出行 支付金额 1 元，实付 13.4 元，订单号 12345678",
                            "avg_score": 0.94,
                            "warnings": [],
                        },
                    ],
                },
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["amount"].value == "13.40元"


def test_user_agent_review_payload_keeps_document_preview_data() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我上传了打车票据，帮我生成报销草稿",
                user_id="pytest",
                context_json={
                    "attachment_names": ["滴滴行程单.png"],
                    "attachment_count": 1,
                    "ocr_documents": [
                        {
                            "filename": "滴滴行程单.png",
                            "summary": "滴滴出行电子行程单",
                            "text": "滴滴出行 实付 13.4 元",
                            "avg_score": 0.94,
                            "preview_kind": "image",
                            "preview_data_url": "data:image/png;base64,ZmFrZQ==",
                            "warnings": [],
                        },
                    ],
                },
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我上传了打车票据，帮我生成报销草稿",
                ontology=ontology,
                context_json={
                    "attachment_names": ["滴滴行程单.png"],
                    "attachment_count": 1,
                    "ocr_documents": [
                        {
                            "filename": "滴滴行程单.png",
                            "summary": "滴滴出行电子行程单",
                            "text": "滴滴出行 实付 13.4 元",
                            "avg_score": 0.94,
                            "preview_kind": "image",
                            "preview_data_url": "data:image/png;base64,ZmFrZQ==",
                            "warnings": [],
                        },
                    ],
                },
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        assert response.review_payload.document_cards[0].preview_kind == "image"
        assert response.review_payload.document_cards[0].preview_data_url.startswith("data:image/png;base64,")


def test_user_agent_review_payload_prechecks_travel_receipts_against_policy_and_hides_old_briefs(
    monkeypatch,
) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        employee = Employee(
            employee_no="E-TRAVEL-001",
            name="张三",
            email="pytest-travel@example.com",
            position="实施顾问",
            grade="P4",
        )
        db.add(employee)
        db.flush()
        db.add(
            ExpenseClaim(
                claim_no="EXP-HISTORY-001",
                employee_id=employee.id,
                employee_name=employee.name,
                department_name="交付部",
                expense_type="travel",
                reason="历史差旅记录",
                location="北京",
                amount=Decimal("680.00"),
                invoice_count=1,
                occurred_at=datetime.now(UTC) - timedelta(days=7),
                status="draft",
                risk_flags_json=[{"label": "历史风险"}],
            )
        )
        db.commit()

        query = "我去北京出差住酒店，上传了北京酒店发票，帮我生成差旅费报销草稿"
        context = {
            "name": "张三",
            "attachment_names": ["北京酒店发票.png"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "北京酒店发票.png",
                    "document_type": "hotel_invoice",
                    "summary": "北京中心酒店 住宿 1 晚 金额 680 元",
                    "text": "北京中心酒店 住宿 1 晚 金额 680 元",
                    "avg_score": 0.96,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "680"},
                        {"key": "merchant", "label": "酒店", "value": "北京中心酒店"},
                    ],
                    "warnings": [],
                }
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-travel@example.com",
                context_json=context,
            )
        )
        service = UserAgentService(db)
        monkeypatch.setattr(
            service,
            "_build_citations",
            lambda payload: [
                UserAgentCitation(
                    source_type="rule",
                    code="rule.expense.travel_risk_control_standard",
                    title="差旅报销风险管控制度",
                    version="v1.1.0",
                    excerpt="住宿费按职级和城市分级限额执行。",
                )
            ],
        )

        response = service.respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-travel@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        titles = [item.title for item in response.review_payload.risk_briefs]
        assert "历史报销画像" not in titles
        assert "制度注意事项" not in titles
        hotel_brief = next(item for item in response.review_payload.risk_briefs if "住宿超标" in item.title)
        combined = f"{hotel_brief.title}\n{hotel_brief.content}\n{hotel_brief.detail}\n{hotel_brief.suggestion}"
        assert "北京酒店发票.png" in combined
        assert "P4-P5" in combined
        assert "680.00" in combined
        assert "450.00" in combined
        assert "公司差旅费报销规则" in combined
        assert "补充超标说明" in combined
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["merchant_name"].value == "北京中心酒店"


def test_user_agent_review_payload_prefers_hotel_invoice_for_hotel_name() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        query = "我去北京出差，上传了火车票和酒店发票，帮我生成差旅费报销草稿"
        context = {
            "name": "张三",
            "attachment_names": ["北京南站火车票.png", "北京中心酒店发票.png"],
            "attachment_count": 2,
            "ocr_documents": [
                {
                    "filename": "北京南站火车票.png",
                    "document_type": "train_ticket",
                    "summary": "广州南至北京南 高铁二等座 金额 560 元",
                    "text": "广州南至北京南 高铁二等座 金额 560 元",
                    "avg_score": 0.95,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "560"},
                    ],
                    "warnings": [],
                },
                {
                    "filename": "北京中心酒店发票.png",
                    "document_type": "hotel_invoice",
                    "summary": "北京中心酒店 住宿 1 晚 金额 450 元",
                    "text": "北京中心酒店 住宿 1 晚 金额 450 元",
                    "avg_score": 0.96,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "450"},
                        {"key": "merchant", "label": "酒店名称", "value": "北京中心酒店"},
                    ],
                    "warnings": [],
                },
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-travel-hotel-name@example.com",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-travel-hotel-name@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["merchant_name"].value == "北京中心酒店"


def test_user_agent_review_payload_does_not_fill_hotel_name_from_train_ticket() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        query = "我去北京出差，先上传一张火车票，酒店发票还没上传，帮我生成差旅费报销草稿"
        context = {
            "name": "张三",
            "grade": "P4",
            "attachment_names": ["北京南站火车票.png"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "北京南站火车票.png",
                    "document_type": "train_ticket",
                    "scene_code": "travel",
                    "scene_label": "差旅票据",
                    "summary": "电子发票（铁路电子客票） 2026-03-04 广州南至北京南 G123 二等座 金额 560 元 中国铁路",
                    "text": "电子发票（铁路电子客票） 2026-03-04 广州南至北京南 G123 二等座 票价 ￥560.00 中国铁路祝您旅途愉快",
                    "avg_score": 0.95,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "560"},
                        {"key": "route", "label": "行程", "value": "广州南-北京南"},
                        {"key": "date", "label": "业务发生时间", "value": "2026-03-04"},
                        {"key": "merchant_name", "label": "商户", "value": "中国铁路"},
                    ],
                    "warnings": [],
                },
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-train-only-hotel-name@example.com",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-train-only-hotel-name@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        slot_map = {item.key: item for item in response.review_payload.slot_cards}
        assert slot_map["merchant_name"].value == ""
        assert "酒店/商户" not in response.review_payload.missing_slots
        assert "酒店的报销票据待上传（必须）" in response.review_payload.missing_slots
        assert response.review_payload.can_proceed is False
        assert [item.action_type for item in response.review_payload.confirmation_actions if item.emphasis == "primary"] == [
            "save_draft"
        ]
        assert "继续下一步" not in [item.label for item in response.review_payload.confirmation_actions]
        assert "酒店住宿发票/住宿清单（必须，当前待上传）" in response.answer
        assert "市内交通/乘车票据（非必须" not in response.answer
        assert "只能保存为草稿" in response.answer or "保存为草稿" in response.answer
        assert "已识别信息：" in response.answer
        assert "酒店住宿发票/住宿清单" in response.answer
        assert "职级：P4" in response.answer
        assert "目的地：北京" in response.answer
        assert "已提交火车：560.00 元" in response.answer
        field_labels = [
            field.label
            for card in response.review_payload.document_cards
            for field in card.fields
        ]
        assert "商户/酒店" not in field_labels
        assert "列车出发时间" in field_labels


def test_user_agent_review_payload_does_not_prompt_when_only_optional_ride_receipt_is_missing() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        query = "我去北京出差，上传了火车票和酒店票，帮我生成差旅费报销草稿"
        context = {
            "name": "张三",
            "grade": "P4",
            "review_form_values": {"occurred_date": "2026-03-04"},
            "attachment_names": ["北京南站火车票.png", "北京酒店发票.png"],
            "attachment_count": 2,
            "ocr_documents": [
                {
                    "filename": "北京南站火车票.png",
                    "document_type": "train_ticket",
                    "scene_code": "travel",
                    "scene_label": "差旅票据",
                    "summary": "电子发票（铁路电子客票） 2026-03-04 广州南至北京南 G123 二等座 金额 560 元",
                    "text": "电子发票（铁路电子客票） 2026-03-04 广州南至北京南 G123 二等座 票价 ￥560.00",
                    "avg_score": 0.95,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "560"},
                        {"key": "route", "label": "行程", "value": "广州南-北京南"},
                    ],
                    "warnings": [],
                },
                {
                    "filename": "北京酒店发票.png",
                    "document_type": "hotel_invoice",
                    "summary": "北京中心酒店 住宿 1 晚 金额 450 元",
                    "text": "北京中心酒店 住宿 1 晚 金额 450 元",
                    "avg_score": 0.96,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "450"},
                        {"key": "merchant", "label": "酒店名称", "value": "北京中心酒店"},
                    ],
                    "warnings": [],
                },
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-travel-optional-ride@example.com",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-travel-optional-ride@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        assert response.review_payload.can_proceed is True
        assert response.review_payload.missing_slots == []
        assert not any(item.title == "差旅票据待补充" for item in response.review_payload.risk_briefs)
        action_types = [item.action_type for item in response.review_payload.confirmation_actions]
        assert "save_draft" in action_types
        assert "next_step" in action_types
        assert "市内交通/乘车票据（非必须" not in response.answer
        assert "继续下一步" in response.answer


def test_user_agent_review_payload_allows_next_step_after_required_travel_receipts_are_complete() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        query = "我去北京出差，上传了火车票、酒店票和打车票，帮我生成差旅费报销草稿"
        context = {
            "name": "张三",
            "grade": "P4",
            "review_form_values": {"occurred_date": "2026-03-04"},
            "attachment_names": ["北京南站火车票.png", "北京酒店发票.png", "北京打车票.png"],
            "attachment_count": 3,
            "ocr_documents": [
                {
                    "filename": "北京南站火车票.png",
                    "document_type": "train_ticket",
                    "scene_code": "travel",
                    "scene_label": "差旅票据",
                    "summary": "电子发票（铁路电子客票） 2026-03-04 广州南至北京南 G123 二等座 金额 560 元",
                    "text": "电子发票（铁路电子客票） 2026-03-04 广州南至北京南 G123 二等座 票价 ￥560.00",
                    "avg_score": 0.95,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "560"},
                        {"key": "route", "label": "行程", "value": "广州南-北京南"},
                        {"key": "date", "label": "日期", "value": "2026-03-04"},
                    ],
                    "warnings": [],
                },
                {
                    "filename": "北京酒店发票.png",
                    "document_type": "hotel_invoice",
                    "summary": "北京中心酒店 住宿 1 晚 金额 450 元",
                    "text": "北京中心酒店 住宿 1 晚 金额 450 元",
                    "avg_score": 0.96,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "450"},
                        {"key": "merchant", "label": "酒店名称", "value": "北京中心酒店"},
                    ],
                    "warnings": [],
                },
                {
                    "filename": "北京打车票.png",
                    "document_type": "taxi_receipt",
                    "summary": "北京网约车 打车票 支付金额 32 元",
                    "text": "北京网约车 打车票 支付金额 32 元",
                    "avg_score": 0.94,
                    "document_fields": [
                        {"key": "amount", "label": "支付金额", "value": "32"},
                    ],
                    "warnings": [],
                },
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-travel-complete@example.com",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-travel-complete@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        assert response.review_payload.can_proceed is True
        assert response.review_payload.missing_slots == []
        action_types = [item.action_type for item in response.review_payload.confirmation_actions]
        assert "save_draft" in action_types
        assert "next_step" in action_types
        assert not any(item.title == "差旅票据待补充" for item in response.review_payload.risk_briefs)
        assert "无需继续上传票据" in response.answer
        assert "当前信息已较完整" in response.answer


def test_user_agent_review_payload_prechecks_taxi_amount_against_rule_standard() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        query = "我去北京出差，上传了一张打车票，帮我生成差旅费报销草稿"
        context = {
            "name": "张三",
            "grade": "P4",
            "attachment_names": ["北京打车票.png"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "北京打车票.png",
                    "document_type": "taxi_receipt",
                    "summary": "北京网约车 打车票 支付金额 360 元",
                    "text": "北京网约车 打车票 支付金额 360 元",
                    "avg_score": 0.95,
                    "document_fields": [
                        {"key": "amount", "label": "支付金额", "value": "360"},
                    ],
                    "warnings": [],
                }
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        amount_brief = next(item for item in response.review_payload.risk_briefs if "交通费金额超标" in item.title)
        combined = f"{amount_brief.title}\n{amount_brief.content}\n{amount_brief.detail}\n{amount_brief.suggestion}"
        assert "北京打车票.png" in combined
        assert "360.00" in combined
        assert "300.00" in combined
        assert "单笔交通金额" in combined
        assert "报销场景提交与附件标准" in combined
        assert amount_brief.level == "high"
        measurement = next(item for item in response.review_payload.risk_briefs if item.title == "附件金额测算异常")
        assert measurement.level == "warning"
        assert "超出标准" in measurement.detail


def test_user_agent_review_payload_does_not_mark_compliant_taxi_amount_as_low_risk() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        query = "我上传一张的士票59.10元，帮我生成交通费报销草稿"
        context = {
            "name": "张三",
            "attachment_names": ["的士1.jpg"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "的士1.jpg",
                    "document_type": "taxi_receipt",
                    "summary": "出租车/网约车票据 支付金额 59.10 元",
                    "text": "的士 车费 59.10 元",
                    "avg_score": 0.95,
                    "document_fields": [
                        {"key": "amount", "label": "支付金额", "value": "59.10"},
                    ],
                    "warnings": [],
                }
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-taxi-pass@example.com",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-taxi-pass@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        risk_titles = [item.title for item in response.review_payload.risk_briefs]
        risk_details = "\n".join(item.detail for item in response.review_payload.risk_briefs)
        assert "附件金额测算结果" not in risk_titles
        assert "附件金额测算异常" not in risk_titles
        assert "测算通过" not in risk_details


def test_user_agent_review_payload_uses_finance_spreadsheet_hotel_amount_standard() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        AgentAssetService(db).list_assets(asset_type=AgentAssetType.RULE.value)
        employee = Employee(
            employee_no="E-TRAVEL-XLSX-001",
            name="测算员工",
            email="pytest-travel-xlsx@example.com",
            position="基层经理",
            grade="P4",
        )
        db.add(employee)
        db.commit()

        query = "测算员工去北京出差住宿，上传了北京酒店发票，帮我生成差旅费报销草稿"
        context = {
            "name": "测算员工",
            "attachment_names": ["北京酒店发票.png"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "北京酒店发票.png",
                    "document_type": "hotel_invoice",
                    "summary": "北京酒店 住宿 1 晚 金额 480 元",
                    "text": "北京酒店 住宿 1 晚 金额 480 元",
                    "avg_score": 0.96,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "480"},
                    ],
                    "warnings": [],
                }
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-travel-xlsx@example.com",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-travel-xlsx@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        hotel_brief = next(item for item in response.review_payload.risk_briefs if "住宿超标" in item.title)
        combined = f"{hotel_brief.content}\n{hotel_brief.detail}"
        assert "480.00" in combined
        assert "450.00" in combined
        assert "公司差旅费报销规则" in combined


def test_user_agent_review_payload_uses_spreadsheet_city_hotel_standard_not_default_tier() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        AgentAssetService(db).list_assets(asset_type=AgentAssetType.RULE.value)

        query = "我去张家口出差住宿，上传了张家口酒店发票，帮我生成差旅费报销草稿"
        context = {
            "name": "张三",
            "grade": "P4",
            "attachment_names": ["张家口酒店发票.png"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "张家口酒店发票.png",
                    "document_type": "hotel_invoice",
                    "summary": "张家口酒店 住宿 1 晚 金额 320 元",
                    "text": "张家口酒店 住宿 1 晚 金额 320 元",
                    "avg_score": 0.96,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "320"},
                    ],
                    "warnings": [],
                }
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-travel-city@example.com",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-travel-city@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        hotel_brief = next(item for item in response.review_payload.risk_briefs if "住宿超标" in item.title)
        combined = f"{hotel_brief.content}\n{hotel_brief.detail}"
        assert "320.00" in combined
        assert "300.00" in combined
        assert "公司差旅费报销规则" in combined


def test_user_agent_review_payload_uses_finance_spreadsheet_meal_allowance_standard() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        AgentAssetService(db).list_assets(asset_type=AgentAssetType.RULE.value)

        query = "我去北京出差，上传了一张餐饮发票，帮我生成差旅费报销草稿"
        context = {
            "name": "张三",
            "grade": "P4",
            "attachment_names": ["北京餐饮发票.png"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "北京餐饮发票.png",
                    "document_type": "meal_receipt",
                    "summary": "北京餐饮发票 金额 90 元",
                    "text": "北京餐饮发票 金额 90 元",
                    "avg_score": 0.96,
                    "document_fields": [
                        {"key": "amount", "label": "金额", "value": "90"},
                    ],
                    "warnings": [],
                }
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest-travel-meal@example.com",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-travel-meal@example.com",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={"draft_only": True},
            )
        )

        assert response.review_payload is not None
        meal_brief = next(item for item in response.review_payload.risk_briefs if "伙食补助标准" in item.title)
        combined = f"{meal_brief.title}\n{meal_brief.content}\n{meal_brief.detail}\n{meal_brief.suggestion}"
        assert "北京餐饮发票.png" in combined
        assert "90.00" in combined
        assert "65.00" in combined
        assert "直辖市/特区" in combined
        assert "公司差旅费报销规则" in combined
        assert meal_brief.level == "high"
        measurement = next(item for item in response.review_payload.risk_briefs if item.title == "附件金额测算异常")
        assert "伙食补助标准 65.00" in measurement.detail
        assert "超出标准" in measurement.detail


def test_user_agent_filters_deprecated_review_risk_briefs() -> None:
    filtered = UserAgentService._filter_deprecated_review_risk_briefs(
        [
            UserAgentReviewRiskBrief(title="历史报销画像", level="info", content="旧画像"),
            UserAgentReviewRiskBrief(title="用户画像", level="info", content="旧画像"),
            UserAgentReviewRiskBrief(title="制度注意事项", level="info", content="旧制度提示"),
            UserAgentReviewRiskBrief(title="住宿超标待说明", level="high", content="保留"),
        ]
    )

    assert [item.title for item in filtered] == ["住宿超标待说明"]


def test_user_agent_submission_blocked_risk_level_only_marks_amount_reasons_high() -> None:
    assert UserAgentService._resolve_submission_blocked_risk_level("住宿金额超出当前职级差标") == "high"
    assert UserAgentService._resolve_submission_blocked_risk_level("缺少直属领导或参与人员信息") == "warning"
    assert UserAgentService._is_submission_exception_explanation_reason("住宿金额超出当前职级差标，且未补充超标说明。")
    assert not UserAgentService._is_submission_exception_explanation_reason("缺少直属领导或参与人员信息")


def test_user_agent_review_payload_shows_ai_precheck_failure_in_main_message() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        query = "我去北京出差住酒店，帮我生成差旅费报销草稿并进入下一步提交"
        context = {
            "name": "张三",
            "attachment_names": ["北京酒店发票.png"],
            "attachment_count": 1,
            "ocr_documents": [
                {
                    "filename": "北京酒店发票.png",
                    "document_type": "hotel_invoice",
                    "summary": "北京酒店 住宿 1 晚 金额 680 元",
                    "text": "北京酒店 住宿 1 晚 金额 680 元",
                    "avg_score": 0.94,
                }
            ],
        }
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest",
                context_json=context,
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message=query,
                ontology=ontology,
                context_json=context,
                tool_payload={
                    "submission_blocked": True,
                    "submission_blocked_reasons": ["住宿金额超出当前职级差标，且未补充超标说明。"],
                },
            )
        )

        assert response.review_payload is not None
        assert response.answer == response.review_payload.body_message
        assert response.answer.startswith("检测到当前单据存在需要说明的超标风险")
        assert "票据会先正常归集到单据中" in response.answer
        assert "附加说明" in response.answer
        assert response.review_payload.can_proceed is False
        blocked_brief = next(item for item in response.review_payload.risk_briefs if item.title == "提交风险提示")
        assert blocked_brief.level == "high"
        assert "不是票据归集阻断条件" in blocked_brief.detail
        assert not any(item.title == "AI预审未通过" for item in response.review_payload.risk_briefs)


def test_user_agent_prompts_existing_draft_association_choice_for_multi_documents() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我上传了两张票据，帮我生成报销草稿",
                user_id="pytest",
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest",
                message="我上传了两张票据，帮我生成报销草稿",
                ontology=ontology,
                context_json={
                    "attachment_names": ["滴滴行程单.png", "餐饮发票.jpg"],
                    "attachment_count": 2,
                    "ocr_documents": [
                        {"filename": "滴滴行程单.png", "summary": "滴滴出行 金额 32 元", "text": "滴滴出行 金额 32 元"},
                        {"filename": "餐饮发票.jpg", "summary": "餐饮发票 金额 68 元", "text": "餐饮发票 金额 68 元"},
                    ],
                },
                tool_payload={
                    "pending_association_decision": True,
                    "association_candidate_claim_no": "EXP-202605-008",
                },
            )
        )

        assert response.review_payload is not None
        assert response.review_payload.can_proceed is False
        assert [item.action_type for item in response.review_payload.confirmation_actions] == [
            "link_to_existing_draft",
            "create_new_claim_from_documents",
        ]
        assert "EXP-202605-008" in response.answer


def test_user_agent_reports_duplicate_invoice_block_when_linking_draft() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        ontology = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="把这张票据关联到已有草稿",
                user_id="pytest-duplicate",
            )
        )

        response = UserAgentService(db).respond(
            UserAgentRequest(
                run_id=ontology.run_id,
                user_id="pytest-duplicate",
                message="把这张票据关联到已有草稿",
                ontology=ontology,
                context_json={
                    "review_action": "link_to_existing_draft",
                    "attachment_names": ["didi-trip.png"],
                    "attachment_count": 1,
                    "ocr_documents": [
                        {
                            "filename": "didi-trip.png",
                            "summary": "滴滴出行 支付金额 32.50 元",
                            "text": "滴滴出行 支付金额 32.50 元",
                        }
                    ],
                },
                tool_payload={
                    "review_action": "link_to_existing_draft",
                    "duplicate_attachment_blocked": True,
                    "duplicate_invoice_blocked": True,
                    "submission_blocked": True,
                    "submission_blocked_reasons": [
                        "检测到本次上传的票据与草稿 EXP-202605-021 中已有票据重复：didi-trip.png。请重新上传不同的票据后再归集。"
                    ],
                    "message": "检测到本次上传的票据与草稿 EXP-202605-021 中已有票据重复：didi-trip.png。请重新上传不同的票据后再归集。",
                    "claim_id": "claim-duplicate",
                    "claim_no": "EXP-202605-021",
                    "status": "blocked",
                },
            )
        )

        assert "重复" in response.answer
        assert "重新上传不同的票据" in response.answer
        assert "已将本次上传" not in response.answer
        assert response.review_payload is not None
        assert response.review_payload.can_proceed is False
