from __future__ import annotations

import json
from datetime import UTC, datetime
from decimal import Decimal

from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app.api.deps import CurrentUserContext, get_current_user, get_db
from app.db.base import Base
from app.main import create_app
from app.models.financial_record import ExpenseClaim
from app.schemas.steward import StewardAttachmentInput, StewardPlanRequest
from app.services.steward_intent_agent import StewardIntentAgentResult
from app.services.steward_planner import StewardPlannerService


class FakeFunctionCallingIntentAgent:
    def detect(self, request, *, base_date, canonical_fields):
        assert "expense_type" in canonical_fields
        assert base_date.isoformat() == "2026-06-04"
        return StewardIntentAgentResult(
            payload={
                "thinking_events": [
                    {
                        "stage": "task_split",
                        "title": "识别复合报销意图",
                        "content": "模型工具调用识别出 1 个报销任务，并关联本次上传的交通附件。",
                    }
                ],
                "tasks": [
                    {
                        "task_type": "reimbursement",
                        "title": "费用报销 2026-06-03 交通",
                        "summary": "报销昨天客户现场沟通产生的交通费。",
                        "confidence": 0.91,
                        "ontology_fields": {
                            "occurred_date": "昨天",
                            "transport_type": "出租车",
                            "reason_value": "客户现场沟通",
                            "expense_type": "交通费",
                            "unregistered_field": "不能进入业务字段",
                        },
                        "missing_fields": ["amount", "transport_type"],
                    }
                ],
                "attachment_groups": [
                    {
                        "target_task_index": 1,
                        "scene": "transport",
                        "scene_label": "交通费用",
                        "attachment_names": ["出租车票.png"],
                        "excluded_attachment_names": ["客户招待发票.jpg"],
                        "confidence": 0.86,
                        "rationale": "出租车票与交通报销任务匹配，招待发票不归入该任务。",
                    }
                ],
            },
            model_call_traces=[
                {
                    "slot": "main",
                    "provider": "OpenAI Compatible",
                    "model": "gpt-test",
                    "attempt": 1,
                    "status": "succeeded",
                }
            ],
        )


class CountingFunctionCallingIntentAgent(FakeFunctionCallingIntentAgent):
    def __init__(self) -> None:
        self.calls = 0

    def detect(self, request, *, base_date, canonical_fields):
        self.calls += 1
        return super().detect(request, base_date=base_date, canonical_fields=canonical_fields)


class CountingNoResultIntentAgent:
    def __init__(self) -> None:
        self.calls = 0

    def detect(self, request, *, base_date, canonical_fields):
        self.calls += 1
        return None


class EmptyFunctionCallingIntentAgent:
    def detect(self, request, *, base_date, canonical_fields):
        return None


class EntertainmentFunctionCallingIntentAgent:
    def detect(self, request, *, base_date, canonical_fields):
        return StewardIntentAgentResult(
            payload={
                "thinking_events": [],
                "tasks": [
                    {
                        "task_type": "reimbursement",
                        "title": "业务招待费报销",
                        "summary": "报销昨天业务招待费。",
                        "confidence": 0.9,
                        "ontology_fields": {
                            "time_range": "昨天",
                            "expense_type": "业务招待费",
                            "reason": "业务招待",
                        },
                        "missing_fields": [],
                    }
                ],
                "attachment_groups": [],
            },
            model_call_traces=[],
        )


class ApplicationFunctionCallingIntentAgent:
    def detect(self, request, *, base_date, canonical_fields):
        return StewardIntentAgentResult(
            payload={
                "thinking_events": [
                    {
                        "stage": "task_split",
                        "title": "识别出差申请",
                        "content": "模型识别到用户要发起北京出差申请，并且后续还有报销事项。",
                    }
                ],
                "tasks": [
                    {
                        "task_type": "expense_application",
                        "title": "北京出差申请",
                        "summary": "明天前往北京出差3天，支撑国网仿生产部署。",
                        "requested_action": "save_draft",
                        "confidence": 0.94,
                        "ontology_fields": {
                            "time_range": "明天",
                            "location": "北京",
                            "expense_type": "差旅",
                            "reason": "支撑国网仿生产部署",
                        },
                        "missing_fields": [],
                    }
                ],
                "attachment_groups": [],
            },
            model_call_traces=[],
        )


class SingleTravelApplicationFunctionCallingIntentAgent:
    def __init__(self) -> None:
        self.calls = 0

    def detect(self, request, *, base_date, canonical_fields):
        self.calls += 1
        return StewardIntentAgentResult(
            payload={
                "thinking_events": [
                    {
                        "stage": "task_split",
                        "title": "识别出差申请草稿",
                        "content": "模型识别到用户要创建上海出差申请，并保存草稿。",
                    }
                ],
                "tasks": [
                    {
                        "task_type": "expense_application",
                        "title": "上海出差申请",
                        "summary": "2026-02-20 至 2026-02-23 前往上海，国网仿生产服务器部署，火车出行。",
                        "requested_action": "save_draft",
                        "confidence": 0.95,
                        "ontology_fields": {
                            "time_range": "2026-02-20 至 2026-02-23",
                            "location": "上海",
                            "expense_type": "差旅",
                            "reason": "国网仿生产服务器部署",
                            "transport_mode": "火车",
                        },
                        "missing_fields": [],
                    }
                ],
                "attachment_groups": [],
            },
            model_call_traces=[
                {
                    "slot": "main",
                    "provider": "OpenAI Compatible",
                    "model": "gpt-test",
                    "attempt": 1,
                    "status": "succeeded",
                }
            ],
        )


class PendingFlowFunctionCallingIntentAgent:
    def detect(self, request, *, base_date, canonical_fields):
        return StewardIntentAgentResult(
            payload={
                "thinking_events": [
                    {
                        "stage": "flow_confirmation",
                        "title": "识别到出差事项但动作不明确",
                        "content": "用户提供了时间、地点和事由，但没有明确要补办申请还是发起报销。",
                    }
                ],
                "pending_flow_confirmation": {
                    "status": "pending",
                    "source_message": request.message,
                    "reason": "缺少申请或报销动作词，需要用户确认流程方向。",
                    "candidate_flows": [
                        {
                            "flow_id": "travel_application",
                            "label": "补办出差申请",
                            "confidence": 0.52,
                            "reason": "这句话可以理解为补办出差申请。",
                            "ontology_fields": {
                                "time_range": "2月20日",
                                "location": "上海",
                                "expense_type": "差旅",
                                "reason": "辅助国网仿生产环境部署",
                            },
                            "missing_fields": ["transport_mode"],
                        },
                        {
                            "flow_id": "travel_reimbursement",
                            "label": "发起费用报销",
                            "confidence": 0.48,
                            "reason": "这句话也可能是在为已发生出差发起报销。",
                            "ontology_fields": {
                                "time_range": "2月20日",
                                "location": "上海",
                                "expense_type": "差旅",
                                "reason": "辅助国网仿生产环境部署",
                            },
                            "missing_fields": [],
                        },
                    ],
                },
                "tasks": [],
                "attachment_groups": [],
            },
            model_call_traces=[],
        )


class AmbiguousApplicationFunctionCallingIntentAgent:
    def detect(self, request, *, base_date, canonical_fields):
        return StewardIntentAgentResult(
            payload={
                "thinking_events": [
                    {
                        "stage": "task_split",
                        "title": "模型直接判定为申请",
                        "content": "模型误把无动作词的历史出差描述直接判定为申请。",
                    }
                ],
                "tasks": [
                    {
                        "task_type": "expense_application",
                        "title": "上海出差申请",
                        "summary": "2月20-23日去上海出差辅助国网仿生产环境部署。",
                        "confidence": 0.9,
                        "ontology_fields": {
                            "time_range": "2月20日",
                            "location": "上海",
                            "expense_type": "差旅",
                            "reason": "辅助国网仿生产环境部署",
                        },
                        "missing_fields": ["transport_mode"],
                    }
                ],
                "attachment_groups": [],
            },
            model_call_traces=[{"status": "succeeded"}],
        )


def _create_steward_test_client_with_db():
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    TestingSessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
    app = create_app()

    def override_db():
        db = TestingSessionLocal()
        try:
            yield db
        finally:
            db.close()

    app.dependency_overrides[get_db] = override_db
    app.dependency_overrides[get_current_user] = lambda: CurrentUserContext(
        username="zhang.xiaoqing",
        name="张小青",
        role_codes=["user"],
        is_admin=False,
        tenant_id="default",
        department_name="产品交付部",
        employee_no="E-STEW-001",
        employee_id="employee-steward-001",
    )
    return TestClient(app), TestingSessionLocal, app


def _build_fast_rule_fallback_steward_planner(_db):
    return StewardPlannerService(intent_agent=EmptyFunctionCallingIntentAgent())


def _patch_steward_endpoint_planner(monkeypatch) -> None:
    monkeypatch.setattr(
        "app.api.v1.endpoints.steward._build_steward_planner",
        _build_fast_rule_fallback_steward_planner,
    )


def _build_endpoint_application_claim(
    *,
    claim_no: str = "AP-202602-001",
    employee_name: str = "张小青",
    status: str = "approved",
) -> ExpenseClaim:
    return ExpenseClaim(
        id=claim_no.lower().replace("-", "_"),
        claim_no=claim_no,
        employee_name=employee_name,
        department_name="产品交付部",
        expense_type="travel_application",
        reason="辅助国网仿生产服务器部署",
        location="上海",
        amount=Decimal("1800.00"),
        currency="CNY",
        invoice_count=0,
        occurred_at=datetime(2026, 2, 20, tzinfo=UTC),
        submitted_at=datetime(2026, 2, 19, tzinfo=UTC),
        status=status,
        approval_stage="关联单据状态",
        risk_flags_json=[
            {
                "source": "application_detail",
                "application_detail": {
                    "application_business_time": "2026-02-20 至 2026-02-23",
                    "location": "上海",
                    "reason": "辅助国网仿生产服务器部署",
                },
            }
        ],
    )


def test_steward_planner_uses_llm_function_calling_plan_when_available() -> None:
    payload = StewardPlanRequest(
        message="\u6211\u60f3\u7533\u8bf7\u0037\u6708\u0032\u65e5\u53bb\u5317\u4eac\u51fa\u5dee\uff0c\u5e76\u4e14\u6211\u8981\u62a5\u9500\u6628\u5929\u5ba2\u6237\u73b0\u573a\u6c9f\u901a\u7684\u4ea4\u901a\u8d39",
        client_now_iso="2026-06-04T09:30:00+08:00",
        attachments=[
            StewardAttachmentInput(name="出租车票.png"),
            StewardAttachmentInput(name="客户招待发票.jpg"),
        ],
    )

    result = StewardPlannerService(intent_agent=FakeFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "llm_function_call"
    assert result.model_call_traces[0]["status"] == "succeeded"
    assert len(result.tasks) == 1
    fields = result.tasks[0].ontology_fields
    assert fields["time_range"] == "2026-06-03"
    assert fields["transport_mode"] == "taxi"
    assert fields["reason"] == "客户现场沟通"
    assert fields["expense_type"] == "transport"
    assert "occurred_date" not in fields
    assert "transport_type" not in fields
    assert "reason_value" not in fields
    assert "unregistered_field" not in fields
    assert result.tasks[0].missing_fields == ["amount"]
    assert result.attachment_groups[0].attachment_names == ["出租车票.png"]
    assert result.attachment_groups[0].excluded_attachment_names == ["客户招待发票.jpg"]
    assert result.thinking_events[0].stage == "llm_function_call"


def test_steward_planner_normalizes_llm_business_entertainment_expense_type() -> None:
    payload = StewardPlanRequest(
        message="\u6211\u60f3\u7533\u8bf7\u0037\u6708\u0032\u65e5\u53bb\u5317\u4eac\u51fa\u5dee\uff0c\u5e76\u4e14\u62a5\u9500\u6628\u5929\u4e1a\u52a1\u62db\u5f85\u8d39",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService(intent_agent=EntertainmentFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "llm_function_call"
    assert result.tasks[0].ontology_fields["expense_type"] == "entertainment"
    assert result.tasks[0].ontology_fields["time_range"] == "2026-06-03"


def test_steward_planner_enforces_application_transport_gap_after_function_calling() -> None:
    payload = StewardPlanRequest(
        message="\u6211\u60f3\u7533\u8bf7\u660e\u5929\u51fa\u5dee\u5317\u4eac\u0033\u5929\uff0c\u652f\u6491\u56fd\u7f51\u4eff\u751f\u4ea7\u90e8\u7f72\uff0c\u5e76\u4e14\u6211\u8981\u62a5\u9500\u6628\u5929\u7684\u4ea4\u901a\u8d39",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService(intent_agent=ApplicationFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "llm_function_call"
    assert result.tasks[0].requested_action == "save_draft"
    assert result.tasks[0].missing_fields == ["transport_mode"]
    gap_events = [event for event in result.thinking_events if event.stage == "business_gap_check"]
    assert gap_events
    assert "没有说明出行方式" in gap_events[0].content
    assert "火车、飞机或轮船" in gap_events[0].content


def test_steward_planner_returns_pending_flow_confirmation_from_llm() -> None:
    payload = StewardPlanRequest(
        message="2月20-23日去上海出差辅助国网仿生产环境部署",
        client_now_iso="2026-06-15T09:30:00+08:00",
    )

    result = StewardPlannerService(intent_agent=PendingFlowFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "llm_function_call"
    assert result.next_action == "confirm_flow"
    assert result.plan_status == "needs_flow_confirmation"
    assert result.pending_flow_confirmation.status == "pending"
    assert [item.flow_id for item in result.candidate_flows] == [
        "travel_application",
        "travel_reimbursement",
    ]
    assert result.candidate_flows[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23"
    assert result.candidate_flows[0].ontology_fields["location"] == "上海"
    assert "申请" in result.summary and "报销" in result.summary


def test_steward_planner_tries_llm_before_rule_fallback_for_single_ambiguous_travel_flow() -> None:
    payload = StewardPlanRequest(
        message="\u0032\u6708\u0032\u0030-\u0032\u0033\u65e5\u53bb\u4e0a\u6d77\u51fa\u5dee\u8f85\u52a9\u56fd\u7f51\u4eff\u751f\u4ea7\u73af\u5883\u90e8\u7f72",
        client_now_iso="2026-06-15T09:30:00+08:00",
    )

    intent_agent = CountingNoResultIntentAgent()

    result = StewardPlannerService(intent_agent=intent_agent).build_plan(payload)

    assert intent_agent.calls == 1
    assert result.planning_source == "rule_fallback"
    assert result.next_action == "confirm_flow"
    assert result.plan_status == "needs_flow_confirmation"
    assert result.model_call_traces == []
    assert [item.flow_id for item in result.candidate_flows] == [
        "travel_application",
        "travel_reimbursement",
    ]


def test_steward_planner_uses_llm_for_multi_financial_demands() -> None:
    payload = StewardPlanRequest(
        message="\u6211\u60f3\u7533\u8bf7\u0037\u6708\u0032\u65e5\u53bb\u5317\u4eac\u51fa\u5dee\uff0c\u5e76\u4e14\u6211\u8981\u62a5\u9500\u6628\u5929\u7684\u4ea4\u901a\u8d39",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )
    intent_agent = CountingFunctionCallingIntentAgent()

    result = StewardPlannerService(intent_agent=intent_agent).build_plan(payload)

    assert intent_agent.calls == 1
    assert result.planning_source == "llm_function_call"
    assert result.model_call_traces[0]["status"] == "succeeded"


def test_steward_planner_uses_llm_for_single_explicit_travel_save_draft() -> None:
    payload = StewardPlanRequest(
        message="2026-02-20 至 2026-02-23，上海出差，国网仿生产服务器部署，火车，保存草稿。",
        client_now_iso="2026-06-24T14:20:00+08:00",
    )
    intent_agent = SingleTravelApplicationFunctionCallingIntentAgent()

    result = StewardPlannerService(intent_agent=intent_agent).build_plan(payload)

    assert intent_agent.calls == 1
    assert result.planning_source == "llm_function_call"
    assert result.tasks[0].requested_action == "save_draft"
    assert result.tasks[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23"
    assert result.tasks[0].ontology_fields["reason"] == "国网仿生产服务器部署"
    assert result.model_call_traces[0]["status"] == "succeeded"


def test_steward_planner_rule_fallback_keeps_save_draft_action_and_date_range() -> None:
    payload = StewardPlanRequest(
        message="2026-02-20 至 2026-02-23，上海出差，国网仿生产服务器部署，火车，保存草稿。",
        client_now_iso="2026-06-24T14:20:00+08:00",
    )

    result = StewardPlannerService(intent_agent=EmptyFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "rule_fallback"
    assert result.tasks[0].requested_action == "save_draft"
    assert result.tasks[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23"
    assert result.tasks[0].ontology_fields["reason"] == "国网仿生产服务器部署"


def test_steward_planner_overrides_llm_direct_application_for_ambiguous_travel_flow() -> None:
    payload = StewardPlanRequest(
        message="2月20-23日去上海出差辅助国网仿生产环境部署",
        client_now_iso="2026-06-15T09:30:00+08:00",
    )

    result = StewardPlannerService(intent_agent=AmbiguousApplicationFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "llm_function_call"
    assert result.next_action == "confirm_flow"
    assert result.plan_status == "needs_flow_confirmation"
    assert result.tasks == []
    assert [item.flow_id for item in result.candidate_flows] == [
        "travel_application",
        "travel_reimbursement",
    ]


def test_steward_planner_falls_back_to_rules_when_function_calling_is_unavailable() -> None:
    payload = StewardPlanRequest(
        message="\u6211\u60f3\u7533\u8bf7\u0037\u6708\u0032\u65e5\u53bb\u5317\u4eac\u51fa\u5dee\uff0c\u5e76\u4e14\u6211\u8981\u62a5\u9500\u6628\u5929\u7684\u4ea4\u901a\u8d39",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService(intent_agent=EmptyFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "rule_fallback"
    assert [task.task_type for task in result.tasks] == ["expense_application", "reimbursement"]
    assert result.tasks[0].ontology_fields["time_range"] == "2026-07-02"
    assert result.tasks[1].ontology_fields["time_range"] == "2026-06-03"
    assert result.thinking_events[0].stage == "rule_fallback"


def test_steward_planner_rule_fallback_confirms_ambiguous_travel_flow() -> None:
    payload = StewardPlanRequest(
        message="2月20-23日去上海出差辅助国网仿生产环境部署",
        client_now_iso="2026-06-15T09:30:00+08:00",
    )

    result = StewardPlannerService(intent_agent=EmptyFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "rule_fallback"
    assert result.next_action == "confirm_flow"
    assert result.pending_flow_confirmation.status == "pending"
    assert [item.flow_id for item in result.candidate_flows] == [
        "travel_application",
        "travel_reimbursement",
    ]
    assert result.tasks == []
    assert result.confirmation_groups == []


def test_steward_planner_prefers_application_when_checked_required_application_missing() -> None:
    payload = StewardPlanRequest(
        message="2月20-23日去上海出差辅助国网仿生产服务器部署",
        client_now_iso="2026-06-15T09:30:00+08:00",
        context_json={
            "required_application_gate": {
                "travel": {
                    "checked": True,
                    "candidate_count": 0,
                    "candidates": [],
                }
            }
        },
    )

    result = StewardPlannerService(intent_agent=EmptyFunctionCallingIntentAgent()).build_plan(payload)

    assert result.planning_source == "rule_fallback"
    assert result.next_action == "confirm_flow"
    assert result.pending_flow_confirmation.status == "pending"
    assert [item.flow_id for item in result.candidate_flows] == ["travel_application"]
    assert result.candidate_flows[0].label == "先发起出差申请"
    assert "未查到可关联" in result.pending_flow_confirmation.reason
    assert "先申请" in result.summary


def test_steward_planner_asks_to_link_application_when_checked_required_application_exists() -> None:
    payload = StewardPlanRequest(
        message="2月20-23日去上海出差辅助国网仿生产服务器部署",
        client_now_iso="2026-06-15T09:30:00+08:00",
        context_json={
            "required_application_gate": {
                "travel": {
                    "checked": True,
                    "candidate_count": 2,
                    "candidates": [
                        {"claim_no": "AP-202602-001"},
                        {"claim_no": "AP-202602-002"},
                    ],
                }
            }
        },
    )

    result = StewardPlannerService(intent_agent=EmptyFunctionCallingIntentAgent()).build_plan(payload)

    assert [item.flow_id for item in result.candidate_flows] == [
        "travel_application",
        "travel_reimbursement",
    ]
    assert result.candidate_flows[1].label == "关联已有申请单并发起报销"
    assert "查到 2 个可关联申请单" in result.pending_flow_confirmation.reason
    assert "关联已有申请单" in result.summary


def test_steward_planner_splits_application_and_reimbursement_tasks() -> None:
    payload = StewardPlanRequest(
        message=(
            "我想要申请7月2日去北京出差，辅助北京供电局的税务审核任务，"
            "并且我要报销昨天的交通费，还需要报销6月3日出差去上海的费用"
        ),
        user_id="u001",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService().build_plan(payload)

    assert len(result.tasks) == 3
    assert [task.task_type for task in result.tasks] == [
        "expense_application",
        "reimbursement",
        "reimbursement",
    ]
    assert result.tasks[0].assigned_agent == "application_assistant"
    assert result.tasks[0].ontology_fields["time_range"] == "2026-07-02"
    assert result.tasks[0].ontology_fields["location"] == "北京"
    assert result.tasks[0].ontology_fields["reason"] == "辅助北京供电局的税务审核任务"
    assert result.tasks[1].ontology_fields["time_range"] == "2026-06-03"
    assert result.tasks[1].ontology_fields["expense_type"] == "transport"
    assert result.tasks[2].ontology_fields["time_range"] == "2026-06-03"
    assert result.tasks[2].ontology_fields["location"] == "上海"
    assert result.tasks[2].ontology_fields["expense_type"] == "travel"
    assert all(action.status == "pending" for action in result.confirmation_groups)


def test_steward_planner_keeps_bare_reimbursement_intent_generic() -> None:
    payload = StewardPlanRequest(
        message="我要报销",
        user_id="u001",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService().build_plan(payload)

    assert len(result.tasks) == 1
    task = result.tasks[0]
    assert task.task_type == "reimbursement"
    assert task.assigned_agent == "reimbursement_assistant"
    assert task.ontology_fields.get("expense_type") == "other"
    assert "reason" not in task.ontology_fields
    assert task.missing_fields == ["time_range", "reason"]
    assert [step.action_type for step in task.action_steps] == [
        "fill_reimbursement_fields",
        "build_reimbursement_preview",
        "validate_required_fields",
        "create_reimbursement_draft",
    ]
    assert task.action_steps[-1].status == "blocked"


def test_steward_planner_builds_reimbursement_action_steps() -> None:
    payload = StewardPlanRequest(
        message="我要报销昨天客户现场沟通的交通费",
        user_id="u001",
        client_now_iso="2026-06-04T09:30:00+08:00",
        context_json={"review_form_values": {"amount": "128.50"}},
    )

    result = StewardPlannerService().build_plan(payload)

    assert result.tasks[0].task_type == "reimbursement"
    assert [step.action_type for step in result.tasks[0].action_steps] == [
        "fill_reimbursement_fields",
        "build_reimbursement_preview",
        "validate_required_fields",
        "create_reimbursement_draft",
    ]
    assert result.tasks[0].action_steps[0].payload["ontology_fields"]["amount"] == "128.50"
    assert result.tasks[0].action_steps[-1].status == "planned"


def test_steward_planner_treats_future_travel_without_apply_word_as_application() -> None:
    payload = StewardPlanRequest(
        message="明天出差北京3天，支撑国网仿生产部署，并且报销昨天业务招待费",
        user_id="u001",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService().build_plan(payload)

    assert [task.task_type for task in result.tasks] == [
        "expense_application",
        "reimbursement",
    ]
    assert result.tasks[0].assigned_agent == "application_assistant"
    assert result.tasks[0].ontology_fields["time_range"] == "2026-06-05"
    assert result.tasks[0].ontology_fields["location"] == "北京"
    assert result.tasks[0].ontology_fields["expense_type"] == "travel"
    assert result.tasks[0].ontology_fields["reason"] == "支撑国网仿生产部署"
    assert result.tasks[0].missing_fields == ["transport_mode"]
    gap_events = [event for event in result.thinking_events if event.stage == "business_gap_check"]
    assert gap_events
    assert "没有说明出行方式" in gap_events[0].content
    assert result.tasks[1].assigned_agent == "reimbursement_assistant"
    assert result.tasks[1].ontology_fields["time_range"] == "2026-06-03"
    assert result.tasks[1].ontology_fields["expense_type"] == "entertainment"


def test_steward_planner_outputs_only_canonical_ontology_fields() -> None:
    payload = StewardPlanRequest(
        message="我要报销昨天的交通费",
        client_now_iso="2026-06-04T09:30:00+08:00",
        context_json={
            "review_form_values": {
                "occurred_date": "2026-06-03",
                "transport_type": "taxi",
                "reason_value": "客户现场沟通",
            }
        },
    )

    result = StewardPlannerService().build_plan(payload)

    fields = result.tasks[0].ontology_fields
    assert fields["time_range"] == "2026-06-03"
    assert fields["transport_mode"] == "taxi"
    assert fields["reason"] == "客户现场沟通"
    assert "occurred_date" not in fields
    assert "transport_type" not in fields
    assert "reason_value" not in fields


def test_steward_planner_builds_travel_attachment_group_with_exclusions() -> None:
    payload = StewardPlanRequest(
        message="还需要报销6月3日出差去上海的费用",
        client_now_iso="2026-06-04T09:30:00+08:00",
        attachments=[
            StewardAttachmentInput(name="上海高铁票.jpg"),
            StewardAttachmentInput(name="上海酒店发票.pdf"),
            StewardAttachmentInput(name="出租车票.png"),
            StewardAttachmentInput(name="客户招待发票.jpg"),
        ],
    )

    result = StewardPlannerService().build_plan(payload)

    assert len(result.attachment_groups) == 1
    group = result.attachment_groups[0]
    assert group.scene == "travel"
    assert group.attachment_names == ["上海高铁票.jpg", "上海酒店发票.pdf", "出租车票.png"]
    assert group.excluded_attachment_names == ["客户招待发票.jpg"]
    assert group.confirmation_required is True
    attachment_actions = [
        action for action in result.confirmation_groups if action.action_type == "confirm_attachment_group"
    ]
    assert len(attachment_actions) == 1


def test_steward_stream_endpoint_emits_thinking_before_plan(monkeypatch) -> None:
    _patch_steward_endpoint_planner(monkeypatch)
    client, _session_factory, app = _create_steward_test_client_with_db()

    try:
        with client.stream(
            "POST",
            "/api/v1/steward/plans/stream",
            json={
                "message": "我要报销昨天的交通费",
                "client_now_iso": "2026-06-04T09:30:00+08:00",
            },
        ) as response:
            assert response.status_code == 200
            events = [
                json.loads(line.decode("utf-8") if isinstance(line, bytes) else line)
                for line in response.iter_lines()
                if line
            ]
    finally:
        app.dependency_overrides.clear()

    assert [event["event"] for event in events][:2] == ["thinking", "thinking"]
    assert events[0]["data"]["stage"] == "stream_start"
    assert events[-1]["event"] == "plan"
    assert events[-1]["data"]["tasks"][0]["ontology_fields"]["time_range"] == "2026-06-03"


def test_steward_plan_endpoint_persists_application_and_reimbursement_state(monkeypatch) -> None:
    _patch_steward_endpoint_planner(monkeypatch)
    client, _session_factory, app = _create_steward_test_client_with_db()

    try:
        response = client.post(
            "/api/v1/steward/plans",
            json={
                "message": "我想申请7月2日去北京出差，并且我要报销昨天的交通费",
                "user_id": "u-steward-state",
                "client_now_iso": "2026-06-04T09:30:00+08:00",
                "context_json": {"session_type": "steward", "entry_source": "personal_workbench"},
            },
        )
    finally:
        app.dependency_overrides.clear()

    assert response.status_code == 200
    payload = response.json()
    assert payload["conversation_id"].startswith("conv_")
    state = payload["steward_state"]
    assert state["active_flow"] == "travel_reimbursement"
    assert state["flows"]["travel_application"]["fields"]["location"] == "北京"
    assert state["flows"]["travel_application"]["fields"]["time_range"] == "2026-07-02"
    assert state["flows"]["travel_reimbursement"]["fields"]["time_range"] == "2026-06-03"
    assert state["flows"]["travel_reimbursement"]["fields"]["expense_type"] == "transport"
    assert all("invented_field" not in flow["fields"] for flow in state["flows"].values())


def test_steward_plan_endpoint_queries_applications_before_ambiguous_travel_choice(monkeypatch) -> None:
    _patch_steward_endpoint_planner(monkeypatch)
    client, SessionLocal, app = _create_steward_test_client_with_db()
    try:
        response = client.post(
            "/api/v1/steward/plans",
            json={
                "message": "2月20-23日去上海出差，辅助国网仿生产服务器部署",
                "user_id": "zhang.xiaoqing",
                "client_now_iso": "2026-06-15T09:30:00+08:00",
                "context_json": {
                    "session_type": "steward",
                    "entry_source": "workbench_ai_inline",
                    "name": "张小青",
                    "username": "zhang.xiaoqing",
                },
            },
        )
        assert response.status_code == 200
        payload = response.json()
        assert [item["flow_id"] for item in payload["candidate_flows"]] == ["travel_application"]
        assert payload["candidate_flows"][0]["label"] == "先发起出差申请"
        assert "未查到可关联单据" in payload["pending_flow_confirmation"]["reason"]

        with SessionLocal() as db:
            db.add(_build_endpoint_application_claim())
            db.commit()

        response = client.post(
            "/api/v1/steward/plans",
            json={
                "message": "2月20-23日去上海出差，辅助国网仿生产服务器部署",
                "user_id": "zhang.xiaoqing",
                "client_now_iso": "2026-06-15T09:30:00+08:00",
                "context_json": {
                    "session_type": "steward",
                    "entry_source": "workbench_ai_inline",
                    "name": "张小青",
                    "username": "zhang.xiaoqing",
                },
            },
        )
        assert response.status_code == 200
        payload = response.json()
        assert [item["flow_id"] for item in payload["candidate_flows"]] == [
            "travel_application",
            "travel_reimbursement",
        ]
        assert payload["candidate_flows"][1]["label"] == "关联已有申请单并发起报销"
        assert "查到 1 个可关联申请单" in payload["pending_flow_confirmation"]["reason"]
    finally:
        app.dependency_overrides.clear()


def test_steward_planner_returns_off_topic_for_business_irrelevant_input() -> None:
    payload = StewardPlanRequest(
        message="123",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService().build_plan(payload)

    assert result.plan_status == "off_topic"
    assert result.next_action == "none"
    assert result.tasks == []
    assert result.attachment_groups == []
    assert result.confirmation_groups == []
    assert result.candidate_flows == []
    assert result.planning_source == "rule_fallback"
    assert len(result.suggested_prompts) == 3
    assert result.thinking_events[0].stage == "off_topic"
    # 纯数字应归类为 meaningless 场景
    assert "未识别到财务事项" in result.thinking_events[0].title


def test_steward_planner_returns_off_topic_with_friendly_greeting_reply() -> None:
    payload = StewardPlanRequest(
        message="你好",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService().build_plan(payload)

    assert result.plan_status == "off_topic"
    assert result.next_action == "none"
    assert result.tasks == []
    assert result.candidate_flows == []
    assert result.planning_source == "rule_fallback"
    assert len(result.suggested_prompts) == 3
    # 问候场景应礼貌回应主人，不使用"抱歉/没识别到"等生硬措辞
    assert "您好主人" in result.summary
    assert "很高兴为您服务" in result.summary
    assert "先回应主人的问候" in result.thinking_events[0].title


def test_steward_planner_returns_off_topic_for_pure_punctuation() -> None:
    payload = StewardPlanRequest(
        message="??? !!!",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService().build_plan(payload)

    assert result.plan_status == "off_topic"
    assert result.next_action == "none"
    assert result.tasks == []
    assert result.candidate_flows == []
    assert result.planning_source == "rule_fallback"
    assert len(result.suggested_prompts) == 3
    assert result.thinking_events[0].stage == "off_topic"


def test_steward_planner_returns_off_topic_for_off_business_with_llm_response() -> None:
    """有内容但与业务无关的场景：应优先使用 LLM 生成的引导文案。"""
    llm_text = (
        "### 抱歉主人，这句话我暂时帮不上忙\n\n"
        "主人聊的是天气，目前小财管家只能帮您整理**费用申请**和**费用报销**。"
        "要不您把想办的财务事项告诉我？"
    )

    class _FakeOffTopicAgent:
        def __init__(self) -> None:
            self.calls = 0
            self.last_call_traces: list[dict[str, object]] = []

        def generate(self, request, *, scenario):
            self.calls += 1
            from app.services.steward_off_topic_agent import StewardOffTopicAgentResult

            return StewardOffTopicAgentResult(
                response_text=llm_text,
                model_call_traces=[{"slot": "main", "status": "succeeded", "model": "gpt-test"}],
            )

    agent = _FakeOffTopicAgent()
    payload = StewardPlanRequest(
        message="想问候您一下",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService(off_topic_agent=agent).build_plan(payload)

    assert agent.calls == 1
    assert result.plan_status == "off_topic"
    assert result.summary == llm_text
    assert result.model_call_traces and result.model_call_traces[0]["status"] == "succeeded"
    # 思考事件应是 off_business 场景对应文案
    assert "不在服务范围内" in result.thinking_events[0].title


def test_steward_planner_falls_back_to_template_when_off_topic_agent_raises() -> None:
    """LLM 失败时静默 fallback 到规则模板，不阻断业务无关拦截。"""

    class _ExplodingOffTopicAgent:
        def generate(self, request, *, scenario):
            raise RuntimeError("模型供应商不可用")

    payload = StewardPlanRequest(
        message="想问候您一下",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService(off_topic_agent=_ExplodingOffTopicAgent()).build_plan(payload)

    assert result.plan_status == "off_topic"
    # 仍使用 off_business 场景的默认模板
    assert "抱歉主人" in result.summary
    assert "不在服务范围内" in result.thinking_events[0].title
    assert result.model_call_traces == []


def test_steward_planner_skips_off_topic_agent_for_greeting_and_meaningless() -> None:
    """问候与无意义场景不走 LLM，节省调用。"""

    class _CallCounterOffTopicAgent:
        def __init__(self) -> None:
            self.calls = 0

        def generate(self, request, *, scenario):
            self.calls += 1
            return None

    agent = _CallCounterOffTopicAgent()
    service = StewardPlannerService(off_topic_agent=agent)

    for message in ("你好", "123", "???"):
        result = service.build_plan(StewardPlanRequest(message=message))
        assert result.plan_status == "off_topic"

    assert agent.calls == 0


def test_steward_planner_preserves_normal_business_flow_after_guard() -> None:
    payload = StewardPlanRequest(
        message="我要报销昨天的交通费",
        client_now_iso="2026-06-04T09:30:00+08:00",
    )

    result = StewardPlannerService().build_plan(payload)

    assert result.plan_status != "off_topic"
    assert len(result.tasks) >= 1
    assert [task.task_type for task in result.tasks] == ["reimbursement"]
