from __future__ import annotations

from collections.abc import Generator

import pytest
from auth_helpers import install_legacy_header_auth_override
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

from app.api.deps import get_db
from app.core.agent_enums import AgentName, AgentRunSource, AgentRunStatus
from app.db.base import Base
from app.schemas.ontology import OntologyParseRequest
from app.services.ontology import LlmOntologyParseResult, SemanticOntologyService
from app.services.ontology_field_registry import normalize_ontology_context_json
from app.services.runtime_chat import RuntimeChatCallTrace, RuntimeChatResult


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 build_client() -> tuple[TestClient, sessionmaker[Session]]:
    session_factory = build_session_factory()
    from app.main import create_app

    app = create_app()
    install_legacy_header_auth_override(app)

    def override_db() -> Generator[Session, None, None]:
        db = session_factory()
        try:
            yield db
        finally:
            db.close()

    app.dependency_overrides[get_db] = override_db
    return TestClient(app), session_factory


EVALUATION_CASES = [
    pytest.param(
        "查一下本周报销超标风险",
        "expense",
        "risk_check",
        "read",
        {},
        id="expense-risk-check",
    ),
    pytest.param(
        "张三 4 月差旅报销金额是多少",
        "expense",
        "query",
        "read",
        {},
        id="expense-query-employee-month",
    ),
    pytest.param(
        "为什么酒店超标报销不能直接通过",
        "expense",
        "explain",
        "read",
        {},
        id="expense-explain-policy",
    ),
    pytest.param(
        "列出金额最高的10笔报销",
        "expense",
        "query",
        "read",
        {},
        id="expense-topn-query",
    ),
    pytest.param(
        "帮我生成张三4月差旅报销草稿",
        "expense",
        "draft",
        "draft_write",
        {},
        id="expense-draft",
    ),
    pytest.param(
        "我今天去客户现场，招待了客户，花销了1000元",
        "expense",
        "draft",
        "draft_write",
        {},
        id="expense-narrative-draft",
    ),
    pytest.param(
        "客户 A 这个月还有多少应收",
        "accounts_receivable",
        "query",
        "read",
        {},
        id="ar-query-customer-month",
    ),
    pytest.param(
        "对比客户A和客户B本月应收差异",
        "accounts_receivable",
        "compare",
        "read",
        {},
        id="ar-compare-customers",
    ),
    pytest.param(
        "检查客户B逾期应收风险",
        "accounts_receivable",
        "risk_check",
        "read",
        {},
        id="ar-risk-check",
    ),
    pytest.param(
        "生成客户A回款跟进草稿",
        "accounts_receivable",
        "draft",
        "draft_write",
        {},
        id="ar-draft",
    ),
    pytest.param(
        "查询客户B账龄明细",
        "accounts_receivable",
        "query",
        "read",
        {},
        id="ar-aging-query",
    ),
    pytest.param(
        "供应商 B 明天要付多少钱",
        "accounts_payable",
        "query",
        "read",
        {},
        id="ap-query-vendor-tomorrow",
    ),
    pytest.param(
        "对比供应商A和供应商B本月应付差异",
        "accounts_payable",
        "compare",
        "read",
        {},
        id="ap-compare-vendors",
    ),
    pytest.param(
        "检查供应商B逾期付款风险",
        "accounts_payable",
        "risk_check",
        "read",
        {},
        id="ap-risk-check",
    ),
    pytest.param(
        "生成供应商A付款沟通草稿",
        "accounts_payable",
        "draft",
        "draft_write",
        {},
        id="ap-draft",
    ),
    pytest.param(
        "帮我安排付款给供应商B",
        "accounts_payable",
        "operate",
        "approval_required",
        {"role_codes": ["finance"]},
        id="ap-operate-approval-required",
    ),
    pytest.param(
        "公司财务制度在哪里看",
        "knowledge",
        "query",
        "read",
        {},
        id="knowledge-query",
    ),
    pytest.param(
        "规则中心的审核依据是什么",
        "knowledge",
        "explain",
        "read",
        {},
        id="knowledge-explain",
    ),
    pytest.param(
        "知识库里有没有双人复核制度",
        "knowledge",
        "query",
        "read",
        {},
        id="knowledge-query-library",
    ),
    pytest.param(
        "帮我直接付款给供应商B",
        "accounts_payable",
        "operate",
        "forbidden",
        {"role_codes": ["user"]},
        id="forbidden-direct-payment",
    ),
    pytest.param(
        "帮我上线付款双人复核规则",
        "accounts_payable",
        "operate",
        "forbidden",
        {"role_codes": ["user"]},
        id="forbidden-activate-rule",
    ),
    pytest.param(
        "帮我删除今天的报销记录",
        "expense",
        "operate",
        "forbidden",
        {"role_codes": ["user"]},
        id="forbidden-delete-expense",
    ),
]


@pytest.mark.parametrize("query,scenario,intent,permission,context_json", EVALUATION_CASES)
def test_semantic_ontology_service_matches_day3_evaluation_set(
    query: str,
    scenario: str,
    intent: str,
    permission: str,
    context_json: dict,
) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest",
                context_json=context_json,
            )
        )

        assert result.scenario == scenario
        assert result.intent == intent
        assert result.permission.level == permission
        assert result.run_id.startswith("run_")


def test_semantic_ontology_service_extracts_entities_time_and_constraints() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="张三 2026年4月差旅报销金额超过5000元的明细",
                user_id="pytest",
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "query"
        assert result.time_range.start_date == "2026-04-01"
        assert result.time_range.end_date == "2026-04-30"


def test_semantic_ontology_service_extracts_budget_query_fields() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="查询 CC-4100 2026年度差旅费可用预算和预算占用",
                user_id="pytest",
            )
        )

        entity_map = {item.type: item.normalized_value for item in result.entities}
        metric_names = {item.name for item in result.metrics}

        assert result.scenario == "budget"
        assert result.intent == "query"
        assert entity_map["cost_center"] == "CC-4100"
        assert entity_map["budget_period"] == "2026年度"
        assert entity_map["budget_subject"] == "travel"
        assert entity_map["expense_type"] == "travel"
        assert {"available_amount", "reserved_amount"}.issubset(metric_names)


@pytest.mark.parametrize(
    "query",
    [
        "申请出差",
        "申请差旅",
        "去国网出差3天，协助仿生产环境部署",
        "去北京出差3天，支撑国网仿生产环境部署",
        "下周去上海出差支撑客户系统上线，预计3天",
        "安排去深圳客户现场验收项目，出差两天",
        "准备去国网现场做仿生产环境部署，差旅3天",
    ],
)
def test_semantic_ontology_service_treats_apply_for_travel_as_expense_application(query: str) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query=query,
                user_id="pytest",
            )
        )

        entity_map = {item.type: item.normalized_value for item in result.entities}
        entity_types = {item.type for item in result.entities}

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert result.permission.level == "draft_write"
        assert entity_map["document_type"] == "expense_application"
        assert entity_map["workflow_stage"] == "pre_approval"
        assert entity_map["expense_type"] == "travel"
        assert "employee" not in entity_types
        assert "amount" in result.missing_slots
        assert "time_range" in result.missing_slots


def test_semantic_ontology_service_keeps_explicit_travel_reimbursement_as_reimbursement_draft() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我要报销去北京出差的费用",
                user_id="pytest",
            )
        )

        entity_map = {item.type: item.normalized_value for item in result.entities}

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert entity_map["expense_type"] == "travel"
        assert "document_type" not in entity_map
        assert "workflow_stage" not in entity_map


def test_semantic_ontology_service_extracts_budget_edit_fields() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="编辑预算：2026年度 CC-4100 差旅费预算金额60万元，预警线80%，控制动作提醒",
                user_id="pytest",
                context_json={
                    "document_type": "budget_plan",
                    "entry_source": "budget_center",
                    "conversation_scenario": "budget",
                },
            )
        )

        entity_map = {item.type: item.normalized_value for item in result.entities}

        assert result.scenario == "budget"
        assert result.intent == "draft"
        assert result.permission.level == "draft_write"
        assert entity_map["budget_period"] == "2026年度"
        assert entity_map["budget_subject"] == "travel"
        assert entity_map["expense_type"] == "travel"
        assert entity_map["budget_amount"] == "600000"
        assert entity_map["warning_threshold"] == "80%"
        assert entity_map["control_action"] == "remind"


def test_semantic_ontology_service_extracts_quarter_budget_period() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="查询 CC-4100 2026年Q3 住宿费预算金额",
                user_id="pytest",
            )
        )

        entity_map = {item.type: item.normalized_value for item in result.entities}

        assert result.scenario == "budget"
        assert entity_map["budget_period"] == "2026年Q3"
        assert entity_map["budget_subject"] == "hotel"
        assert entity_map["expense_type"] == "hotel"


@pytest.mark.parametrize(
    "query,expected_code,expected_label",
    [
        ("查询2026年度市场推广费预算余额", "marketing", "市场推广费"),
        ("查看2026年度软件服务费已占用金额", "software", "软件服务费"),
        ("统计2026年度业务招待费预算金额", "meal", "业务招待费"),
    ],
)
def test_semantic_ontology_service_links_budget_subject_to_expense_type(
    query: str,
    expected_code: str,
    expected_label: str,
) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(query=query, user_id="pytest")
        )

        assert result.scenario == "budget"
        assert any(
            item.type == "budget_subject" and item.normalized_value == expected_code
            for item in result.entities
        )
        assert any(
            item.type == "expense_type"
            and item.normalized_value == expected_code
            and item.value == expected_label
            for item in result.entities
        )


def test_semantic_ontology_service_extracts_new_document_numbers() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="查询 RE-20260525103045-ABCDEFGH 和 AP-20260525113045-HGFEDCBA 的状态",
                user_id="pytest",
            )
        )

        claim_codes = {
            item.normalized_value
            for item in result.entities
            if item.type == "expense_claim"
        }
        assert claim_codes == {
            "RE-20260525103045-ABCDEFGH",
            "AP-20260525113045-HGFEDCBA",
        }


def test_semantic_ontology_service_treats_travel_amount_question_as_knowledge_query() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我要去武汉出差3天，请问我一共可以报销多少费用？",
                user_id="pytest",
                context_json={
                    "role_codes": ["employee"],
                    "name": "曹笑竹",
                    "grade": "P3",
                    "session_type": "knowledge",
                },
            )
        )

        assert result.scenario == "knowledge"
        assert result.intent == "query"
        assert result.clarification_required is False
        assert result.clarification_question is None
        assert result.missing_slots == []


def test_semantic_ontology_service_keeps_travel_amount_follow_up_in_knowledge_query() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="那P4员工可以报销多少钱？",
                user_id="pytest",
                context_json={
                    "role_codes": ["employee"],
                    "name": "曹笑竹",
                    "grade": "P3",
                    "session_type": "knowledge",
                    "conversation_history": [
                        {
                            "role": "user",
                            "content": "我要去武汉出差3天，请问我一共可以报销多少费用？",
                        }
                    ],
                },
            )
        )

        assert result.scenario == "knowledge"
        assert result.intent == "query"
        assert result.clarification_required is False


def test_semantic_ontology_service_rejects_draft_intent_inside_knowledge_session(
    monkeypatch,
) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        service = SemanticOntologyService(db)
        monkeypatch.setattr(
            service,
            "_parse_with_model",
            lambda **kwargs: (
                LlmOntologyParseResult(
                    scenario="expense",
                    intent="draft",
                    confidence=0.91,
                    clarification_required=True,
                    clarification_question="请补充招待对象和票据附件。",
                    missing_slots=["participants", "attachments"],
                    ambiguity=[],
                    entity_hints=[],
                ),
                [],
                None,
            ),
        )

        result = service.parse(
            OntologyParseRequest(
                query="我要去北京出差3天，一共可以报销多少钱？",
                user_id="pytest",
                context_json={
                    "role_codes": ["employee"],
                    "name": "曹笑竹",
                    "grade": "P3",
                    "session_type": "knowledge",
                },
            )
        )

        assert result.scenario == "knowledge"
        assert result.intent == "query"
        assert result.clarification_required is False
        assert result.clarification_question is None


def test_review_next_step_context_inherits_expense_draft_flow() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我已核对右侧识别结果，请进入下一步。",
                user_id="pytest",
                context_json={
                    "review_action": "next_step",
                    "draft_claim_id": "claim-1",
                    "attachment_count": 1,
                },
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert result.permission.level == "draft_write"
        assert result.clarification_required is False
        assert result.clarification_question is None


def test_semantic_ontology_service_prefers_expense_for_customer_entertainment_narrative() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我今天去客户现场，招待了客户，花销了1000元",
                user_id="pytest",
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert result.permission.level == "draft_write"
        assert result.time_range.raw == "今天"
        assert result.clarification_required is True
        assert "customer_name" in result.missing_slots
        assert "participants" in result.missing_slots
        assert any(
            item.type == "expense_type" and item.normalized_value == "meal"
            for item in result.entities
        )


def test_semantic_ontology_service_uses_client_local_date_for_relative_time() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = 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,
                },
            )
        )

        assert result.time_range.raw == "昨天"
        assert result.time_range.start_date == "2026-05-12"
        assert result.time_range.end_date == "2026-05-12"


def test_semantic_ontology_service_extracts_day_before_yesterday_from_client_local_date() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = 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,
                },
            )
        )

        assert result.time_range.raw == "前天"
        assert result.time_range.start_date == "2026-05-11"
        assert result.time_range.end_date == "2026-05-11"


def test_semantic_ontology_service_treats_status_document_text_as_query() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="查询草稿的单据",
                user_id="pytest",
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "query"
        assert result.permission.level == "read"
        assert any(
            item.field == "status" and item.value == "draft"
            for item in result.constraints
        )


def test_semantic_ontology_service_extracts_history_query_time_and_location() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我去年去北京报销的单据",
                user_id="pytest",
                context_json={
                    "client_now_iso": "2026-05-21T04:00:00.000Z",
                    "client_timezone_offset_minutes": -480,
                },
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "query"
        assert result.time_range.raw == "去年"
        assert result.time_range.start_date == "2025-01-01"
        assert result.time_range.end_date == "2025-12-31"
        assert any(
            item.type == "location" and item.normalized_value == "北京"
            for item in result.entities
        )


def test_semantic_ontology_service_understands_last_week_claim_progress_query() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我上周提交的单据报销了么？",
                user_id="pytest",
                context_json={
                    "client_now_iso": "2026-05-21T04:00:00.000Z",
                    "client_timezone_offset_minutes": -480,
                },
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "query"
        assert result.time_range.raw == "上周"
        assert result.time_range.start_date == "2026-05-11"
        assert result.time_range.end_date == "2026-05-17"


def test_semantic_ontology_service_maps_office_supplies_to_office_expense_type() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="我买了办公用品和文具，花了88元，帮我报销",
                user_id="pytest",
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert any(
            item.type == "expense_type" and item.normalized_value == "office"
            for item in result.entities
        )


def test_semantic_ontology_service_maps_riding_fare_to_transport_expense_type() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="业务发生时间:2026-03-04，送客户去林萃小区办事，请报销乘车费用",
                user_id="pytest",
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert any(
            item.type == "expense_type" and item.normalized_value == "transport"
            for item in result.entities
        )


def test_semantic_ontology_service_maps_taxi_ticket_reimbursement_to_transport_draft() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="送客户去机场，报销的士票",
                user_id="pytest",
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert any(
            item.type == "expense_type" and item.normalized_value == "transport"
            for item in result.entities
        )
        assert not any(
            item.type == "expense_type" and item.normalized_value == "entertainment"
            for item in result.entities
        )


@pytest.mark.parametrize(
    "query,expected_type",
    [
        ("报销飞机票和行程单", "travel"),
        ("报销酒店发票和房费", "hotel"),
        ("报销滴滴打车票", "transport"),
        ("报销工作餐餐费", "meal"),
        ("报销会议场地费", "meeting"),
        ("报销客户接待餐", "meal"),
        ("报销打印纸和硒鼓", "office"),
        ("报销培训课程费", "training"),
        ("报销手机话费和流量费", "communication"),
        ("报销员工体检费", "welfare"),
    ],
)
def test_semantic_ontology_service_covers_common_expense_scene_keywords(
    query: str,
    expected_type: str,
) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(query=query, user_id="pytest")
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert any(
            item.type == "expense_type" and item.normalized_value == expected_type
            for item in result.entities
        )


def test_semantic_ontology_service_connects_expense_application_to_ontology() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="申请2026-06-01 ~ 2026-06-03去北京做客户现场验收，差旅预算18000元",
                user_id="pytest",
                context_json={
                    "document_type": "expense_application",
                    "application_stage": "pre_approval",
                    "entry_source": "documents_application",
                },
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert any(
            item.type == "document_type" and item.normalized_value == "expense_application"
            for item in result.entities
        )
        assert any(
            item.type == "workflow_stage" and item.normalized_value == "pre_approval"
            for item in result.entities
        )
        assert any(
            item.field == "document_type" and item.value == "expense_application"
            for item in result.constraints
        )
        assert any(
            item.type == "expense_type" and item.normalized_value == "travel"
            for item in result.entities
        )


def test_semantic_ontology_service_requires_attachment_for_meeting_application() -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        result = SemanticOntologyService(db).parse(
            OntologyParseRequest(
                query="发起会务申请，2026-06-01 ~ 2026-06-02上海产品发布会，预算32000元",
                user_id="pytest",
                context_json={
                    "document_type": "expense_application",
                    "application_stage": "pre_approval",
                    "entry_source": "documents_application",
                    "attachment_count": 0,
                },
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert any(
            item.type == "expense_type" and item.normalized_value == "meeting"
            for item in result.entities
        )
        assert "attachments" in result.missing_slots


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

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert any(
            item.type == "document_type" and item.normalized_value == "expense_application"
            for item in result.entities
        )
        assert any(
            item.type == "workflow_stage" and item.normalized_value == "pre_approval"
            for item in result.entities
        )
        assert "expense_type" in result.missing_slots
        assert "amount" in result.missing_slots


def test_semantic_ontology_service_normalizes_business_aliases_to_ontology_fields(
    monkeypatch,
) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        service = SemanticOntologyService(db)
        monkeypatch.setattr(
            service,
            "_parse_with_model",
            lambda **kwargs: (None, [], "model_disabled_for_field_registry_test"),
        )

        result = service.parse(
            OntologyParseRequest(
                query="生成差旅费报销草稿",
                user_id="pytest",
                context_json={
                    "review_action": "save_draft",
                    "review_form_values": {
                        "reimbursement_type": "差旅费",
                        "business_time": "2026-06-01 至 2026-06-03",
                        "business_location": "上海",
                        "reason_value": "支撑国网仿生产环境部署",
                        "application_amount": "3000元",
                        "transport_type": "火车",
                    },
                },
            )
        )

        entity_map = {(item.type, item.normalized_value) for item in result.entities}
        assert ("transport_mode", "火车") in entity_map
        assert ("reason", "支撑国网仿生产环境部署") in entity_map
        assert ("location", "上海") in entity_map
        assert "time_range" not in result.missing_slots
        assert "reason" not in result.missing_slots


def test_ontology_context_normalizes_employee_profile_aliases() -> None:
    context = normalize_ontology_context_json(
        {
            "name": "曹笑竹",
            "department": "技术部",
            "position": "财务智能化产品经理",
            "grade": "P5",
            "managerName": "向万红",
            "costCenter": "TECH-DEPT",
        }
    )

    assert context["employee_name"] == "曹笑竹"
    assert context["department_name"] == "技术部"
    assert context["employee_position"] == "财务智能化产品经理"
    assert context["employee_grade"] == "P5"
    assert context["manager_name"] == "向万红"
    assert context["cost_center"] == "TECH-DEPT"


def test_semantic_ontology_service_uses_model_parse_when_available(monkeypatch) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        service = SemanticOntologyService(db)
        monkeypatch.setattr(
            service,
            "_parse_with_model",
            lambda **kwargs: (
                LlmOntologyParseResult(
                    scenario="expense",
                    intent="draft",
                    confidence=0.91,
                    clarification_required=True,
                    clarification_question="请补充费用类型、金额和票据附件。",
                    missing_slots=["expense_type", "amount", "attachments"],
                    ambiguity=[],
                    entity_hints=[],
                ),
                [
                    {
                        "slot": "main",
                        "provider": "MiniMax",
                        "model": "intent-model",
                        "attempt": 1,
                        "status": "succeeded",
                        "duration_ms": 8,
                    }
                ],
                None,
            ),
        )

        result = service.parse(
            OntologyParseRequest(
                query="我要报销",
                user_id="pytest",
            )
        )

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert result.parse_strategy == "llm_primary"
        assert result.clarification_required is True
        assert "expense_type" in result.missing_slots
        assert result.clarification_question == "请补充费用类型、金额和票据附件。"


def test_semantic_ontology_service_falls_back_when_model_conflicts_with_application_signal(
    monkeypatch,
) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        service = SemanticOntologyService(db)

        monkeypatch.setattr(
            service.runtime_chat_service,
            "complete_with_trace",
            lambda *args, **kwargs: RuntimeChatResult(
                text=(
                    '{"scenario":"knowledge","intent":"query","confidence":0.91,'
                    '"clarification_required":false,"missing_slots":[],'
                    '"ambiguity":[],"entity_hints":[]}'
                ),
                calls=[
                    RuntimeChatCallTrace(
                        slot="main",
                        provider="MiniMax",
                        model="intent-model",
                        attempt=1,
                        status="succeeded",
                        duration_ms=11,
                    )
                ],
            ),
        )

        result = service.parse(
            OntologyParseRequest(
                query="去国网出差3天，协助仿生产环境部署",
                user_id="pytest",
            )
        )
        fetched = service.run_service.get_run(result.run_id)

        entity_map = {item.type: item.normalized_value for item in result.entities}

        assert result.scenario == "expense"
        assert result.intent == "draft"
        assert result.parse_strategy == "rule_fallback"
        assert entity_map["document_type"] == "expense_application"
        assert fetched is not None
        assert fetched.tool_calls[0].status == "failed"
        assert fetched.tool_calls[0].error_message == "model_conflicts_with_application_stage_signal"


def test_semantic_ontology_service_records_model_call_errors_for_statistics(monkeypatch) -> None:
    session_factory = build_session_factory()
    with session_factory() as db:
        service = SemanticOntologyService(db)
        run = service.run_service.create_run(
            agent=AgentName.ORCHESTRATOR.value,
            source=AgentRunSource.USER_MESSAGE.value,
            tenant_id="default",
            status=AgentRunStatus.RUNNING.value,
        )

        monkeypatch.setattr(
            service.runtime_chat_service,
            "complete_with_trace",
            lambda *args, **kwargs: RuntimeChatResult(
                text=None,
                calls=[
                    RuntimeChatCallTrace(
                        slot="main",
                        provider="MiniMax",
                        model="intent-model",
                        attempt=1,
                        status="failed",
                        duration_ms=15,
                        error_message="incorrect api key",
                    )
                ],
            ),
        )

        result = service.parse_for_run(
            OntologyParseRequest(
                query="去北京出差3天，支撑国网仿生产环境部署",
                user_id="pytest",
            ),
            run_id=run.run_id,
        )
        fetched = service.run_service.get_run(run.run_id)
        stats = service.run_service.summarize_runs(limit=20)

        assert result.parse_strategy == "rule_fallback"
        assert fetched is not None
        assert len(fetched.tool_calls) == 1
        assert fetched.tool_calls[0].tool_name == "semantic_ontology.main"
        assert fetched.tool_calls[0].status == "failed"
        assert fetched.tool_calls[0].error_message == "incorrect api key"
        assert stats.failed_llm_call_count >= 1


def test_parse_ontology_endpoint_returns_eight_fields_and_writes_trace() -> None:
    client, _ = build_client()

    response = client.post(
        "/api/v1/ontology/parse",
        json={
            "query": "查一下本周报销超标风险",
            "user_id": "pytest",
            "context_json": {"role_codes": ["finance"]},
        },
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["scenario"] == "expense"
    assert payload["intent"] == "risk_check"
    assert payload["permission"]["level"] == "read"
    assert payload["run_id"].startswith("run_")
    assert set(payload) >= {
        "scenario",
        "intent",
        "entities",
        "time_range",
        "metrics",
        "constraints",
        "risk_flags",
        "permission",
        "confidence",
        "missing_slots",
        "ambiguity",
        "parse_strategy",
        "clarification_required",
        "clarification_question",
        "run_id",
        "field_errors",
    }

    run_response = client.get(f"/api/v1/agent-runs/{payload['run_id']}")

    assert run_response.status_code == 200
    run_payload = run_response.json()
    assert run_payload["ontology_json"]["scenario"] == "expense"
    assert run_payload["ontology_json"]["intent"] == "risk_check"
    assert run_payload["semantic_parse"]["scenario"] == "expense"
    assert run_payload["semantic_parse"]["intent"] == "risk_check"


def test_parse_ontology_endpoint_blocks_non_business_input() -> None:
    client, _ = build_client()

    response = client.post(
        "/api/v1/ontology/parse",
        json={
            "query": "你好",
            "user_id": "pytest",
        },
    )

    assert response.status_code == 400
    assert "财务业务相关问题" in response.json()["detail"]


def test_parse_ontology_endpoint_returns_forbidden_for_unprivileged_payment_request() -> None:
    client, _ = build_client()

    response = client.post(
        "/api/v1/ontology/parse",
        json={
            "query": "帮我直接付款给供应商B",
            "user_id": "pytest",
            "context_json": {"role_codes": ["user"]},
        },
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["scenario"] == "accounts_payable"
    assert payload["intent"] == "operate"
    assert payload["permission"]["level"] == "forbidden"
    assert payload["clarification_required"] is True
    assert payload["field_errors"]
