from __future__ import annotations

import base64
import json
from collections.abc import Generator
from datetime import UTC, date, datetime
from decimal import Decimal

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

from app.api.deps import get_db
from app.db.base import Base
from app.main import create_app
from app.models.ai_learning import AIDecision, AIDecisionFeedback, WorkflowOutcome
from app.models.approval_action import ApprovalActionLedger
from app.models.budget import BudgetAllocation, BudgetReservation, BudgetTransaction
from app.models.employee import Employee
from app.models.expense_case import BusinessEvent, ExpenseCaseLink
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.organization import OrganizationUnit
from app.models.risk_observation import RiskObservation, RiskObservationFeedback
from app.models.role import Role
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead
from app.schemas.ontology import OntologyParseResult
from app.schemas.user_agent import UserAgentRequest
from app.services.document_preview import DocumentPreviewAssets
from app.services.expense_cases import ExpenseCaseService
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
from app.services.expense_claims import ExpenseClaimService
from app.services.ocr import OcrService
from app.services.user_agent import UserAgentService


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()
    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


def seed_claim(db: Session) -> tuple[ExpenseClaim, ExpenseClaimItem]:
    manager = Employee(
        id="mgr-1",
        employee_no="E20001",
        name="李总",
        email="manager@example.com",
        position="市场总监",
        grade="P7",
    )
    role = Role(
        id="role-user",
        role_code="user",
        name="员工",
        description="普通员工",
    )
    employee = Employee(
        id="emp-1",
        employee_no="E10001",
        name="张三",
        email="zhangsan@example.com",
        position="招商主管",
        grade="P4",
        manager=manager,
        roles=[role],
    )
    claim = ExpenseClaim(
        id="claim-attachment-1",
        claim_no="EXP-202605-101",
        employee_id=employee.id,
        employee_name="张三",
        department_id="dept-1",
        department_name="市场部",
        project_code=None,
        expense_type="office",
        reason="办公用品采购",
        location="深圳南山",
        amount=Decimal("88.00"),
        currency="CNY",
        invoice_count=0,
        occurred_at=datetime(2026, 5, 13, tzinfo=UTC),
        submitted_at=None,
        status="draft",
        approval_stage="待提交",
        risk_flags_json=[],
    )
    item = ExpenseClaimItem(
        id="item-attachment-1",
        claim_id=claim.id,
        item_date=date(2026, 5, 13),
        item_type="office",
        item_reason="办公用品采购",
        item_location="深圳南山",
        item_amount=Decimal("88.00"),
        invoice_id=None,
    )
    claim.items = [item]
    db.add(manager)
    db.add(role)
    db.add(employee)
    db.add(claim)
    db.commit()
    return claim, item


def test_claim_pre_review_and_submit_share_structured_handshake() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim.invoice_count = 1
        item.invoice_id = "office-receipt.png"
        db.commit()

    headers = {
        "x-auth-username": "zhangsan@example.com",
        "x-request-id": "pre-review-endpoint-ready-1",
    }
    review_response = client.post(
        "/api/v1/reimbursements/claims/claim-attachment-1/pre-review",
        headers=headers,
    )

    assert review_response.status_code == 200
    review = review_response.json()["pre_review"]
    assert review["decision"] in {"ready", "ready_with_review"}
    assert review["review_id"]
    assert review["input_fingerprint"].startswith("sha256:")
    assert review["rule_set_fingerprint"].startswith("sha256:")

    submit_response = client.post(
        "/api/v1/reimbursements/claims/claim-attachment-1/submit",
        headers={"x-auth-username": "zhangsan@example.com"},
        json={
            "pre_review_id": review["review_id"],
            "input_fingerprint": review["input_fingerprint"],
        },
    )

    assert submit_response.status_code == 200
    assert submit_response.json()["status"] == "submitted"
    with session_factory() as db:
        events = list(
            db.scalars(
                select(BusinessEvent)
                .where(BusinessEvent.aggregate_id == "claim-attachment-1")
                .order_by(BusinessEvent.occurred_at.asc())
            ).all()
        )
        assert [event.event_type for event in events] == [
            "claim_pre_review_completed",
            "claim_submitted",
        ]
        assert events[0].correlation_id == "pre-review-endpoint-ready-1"
        assert events[1].correlation_id == events[0].correlation_id
        assert events[1].causation_id == events[0].id


def test_claim_submit_returns_structured_pre_review_conflict() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim.invoice_count = 1
        item.invoice_id = "wrong-receipt.png"
        claim.risk_flags_json = [
            {
                "source": "manual_risk",
                "severity": "high",
                "actionability": "fixable_by_submitter",
                "business_stage": "reimbursement",
                "label": "票据与明细不一致",
                "message": "请更正费用明细或重新上传正确票据。",
                "item_ids": [item.id],
            }
        ]
        db.commit()

    response = client.post(
        "/api/v1/reimbursements/claims/claim-attachment-1/submit",
        headers={"x-auth-username": "zhangsan@example.com"},
        json={},
    )

    assert response.status_code == 409
    detail = response.json()["detail"]
    assert detail["code"] == "PRE_REVIEW_NEEDS_FIX"
    assert detail["review"]["decision"] == "needs_fix"
    assert detail["review"]["blocking_count"] == 1
    assert detail["review"]["findings"][0]["remediation"]["action"] == "edit_item_note"
    with session_factory() as db:
        claim = db.get(ExpenseClaim, "claim-attachment-1")
        assert claim is not None
        assert claim.status == "draft"
        assert claim.submitted_at is None
        assert (
            db.scalar(
                select(BusinessEvent).where(
                    BusinessEvent.aggregate_id == claim.id,
                    BusinessEvent.event_type == "claim_submitted",
                )
            )
            is None
        )


def test_claim_submit_returns_changed_conflict_when_dynamic_review_changes(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim.invoice_count = 1
        item.invoice_id = "office-receipt.png"
        db.commit()

    headers = {"x-auth-username": "zhangsan@example.com"}
    review_response = client.post(
        "/api/v1/reimbursements/claims/claim-attachment-1/pre-review",
        headers=headers,
    )
    assert review_response.status_code == 200
    review = review_response.json()["pre_review"]
    assert review["decision"] in {"ready", "ready_with_review"}

    def changed_review(_service, _claim):
        return {
            "risk_flags": [
                {
                    "source": "submission_review",
                    "severity": "medium",
                    "actionability": "review_decision",
                    "business_stage": "reimbursement",
                    "label": "新增历史风险",
                    "message": "预审后发现新的历史风险记录，请确认后重试。",
                }
            ],
            "rule_set_fingerprint": "rules-v1",
        }

    monkeypatch.setattr(
        ExpenseClaimService,
        "_run_ai_submission_review",
        changed_review,
    )
    response = client.post(
        "/api/v1/reimbursements/claims/claim-attachment-1/submit",
        headers=headers,
        json={
            "pre_review_id": review["review_id"],
            "input_fingerprint": review["input_fingerprint"],
        },
    )

    assert response.status_code == 409
    detail = response.json()["detail"]
    assert detail["code"] == "PRE_REVIEW_CHANGED"
    assert detail["review"]["review_id"] != review["review_id"]
    assert detail["review"]["decision"] == "ready_with_review"
    with session_factory() as db:
        claim = db.get(ExpenseClaim, "claim-attachment-1")
        assert claim is not None
        assert claim.status == "draft"


def test_claim_read_uses_organization_manager_and_dedupes_budget_warnings() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        department = OrganizationUnit(
            id="dept-org-manager",
            unit_code="DEPT-ORG-MANAGER",
            name="交付部",
            manager_name="王总",
        )
        employee = Employee(
            id="emp-org-manager",
            employee_no="E30001",
            name="赵六",
            email="zhaoliu@example.com",
            organization_unit=department,
            position="实施顾问",
            grade="P5",
            finance_owner_name="Wang Finance",
        )
        duplicated_warning = {
            "source": "budget_control",
            "event_type": "budget_warning",
            "severity": "medium",
            "label": "预算接近预警线",
            "message": "预算 SIM-BUD-2026-R0048 本次占用后使用率预计达到 99.27%，已达到预警线 80.00%。",
            "budget_no": "SIM-BUD-2026-R0048",
            "allocation_id": "allocation-0048",
            "subject_code": "travel",
        }
        claim = ExpenseClaim(
            id="claim-org-manager",
            claim_no="EXP-202606-ORG-MGR",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            project_code=None,
            expense_type="travel",
            reason="差旅报销",
            location="上海",
            amount=Decimal("880.00"),
            currency="CNY",
            invoice_count=1,
            occurred_at=datetime(2026, 6, 3, tzinfo=UTC),
            submitted_at=datetime(2026, 6, 3, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage="直属领导审批",
            risk_flags_json=[
                {**duplicated_warning, "created_at": "2026-06-03T10:00:00+00:00"},
                {**duplicated_warning, "created_at": "2026-06-03T10:01:00+00:00"},
            ],
        )
        db.add_all([department, employee, claim])
        db.commit()

    headers = {"x-auth-username": "zhaoliu@example.com"}
    response = client.get("/api/v1/reimbursements/claims/claim-org-manager", headers=headers)
    assert response.status_code == 200
    payload = response.json()
    assert payload["manager_name"] == "王总"
    assert payload["finance_owner_name"] == "Wang Finance"
    budget_warnings = [
        flag
        for flag in payload["risk_flags_json"]
        if flag.get("source") == "budget_control" and flag.get("event_type") == "budget_warning"
    ]
    assert len(budget_warnings) == 1
    assert budget_warnings[0]["message"] == duplicated_warning["message"]


def test_claim_read_attaches_finance_approver_name_for_finance_stage() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        finance_role = Role(
            id="role-finance-reader",
            role_code="finance",
            name="财务",
            description="可处理财务复核任务",
        )
        applicant = Employee(
            id="emp-finance-stage-applicant",
            employee_no="E30002",
            name="钱七",
            email="qianqi@example.com",
            position="实施顾问",
            grade="P5",
            finance_owner_name="Wang Finance Group",
        )
        finance_user = Employee(
            id="emp-finance-stage-approver",
            employee_no="F30002",
            name="Wang Finance",
            email="wang.finance@example.com",
            position="财务专员",
            grade="P6",
            finance_owner_name="Wang Finance Group",
            roles=[finance_role],
        )
        claim = ExpenseClaim(
            id="claim-finance-stage-reader",
            claim_no="EXP-202606-FINANCE-MGR",
            employee_id=applicant.id,
            employee_name=applicant.name,
            department_id=None,
            department_name="交付部",
            project_code=None,
            expense_type="travel",
            reason="差旅报销",
            location="上海",
            amount=Decimal("880.00"),
            currency="CNY",
            invoice_count=1,
            occurred_at=datetime(2026, 6, 3, tzinfo=UTC),
            submitted_at=datetime(2026, 6, 3, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage="财务审批",
            risk_flags_json=[],
        )
        db.add_all([finance_role, applicant, finance_user, claim])
        db.commit()

    headers = {"x-auth-username": "qianqi@example.com"}
    response = client.get(
        "/api/v1/reimbursements/claims/claim-finance-stage-reader", headers=headers
    )
    assert response.status_code == 200
    payload = response.json()
    assert payload["finance_owner_name"] == "Wang Finance Group"
    assert payload["finance_approver_name"] == "Wang Finance"


def test_claim_standard_adjustment_endpoint_recalculates_and_marks_reviewer_notice() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim.expense_type = "hotel"
        claim.location = "北京"
        claim.amount = Decimal("1000.00")
        item.item_type = "hotel_ticket"
        item.item_reason = "北京住宿"
        item.item_location = "北京"
        item.item_amount = Decimal("1000.00")
        db.commit()
        claim_id = claim.id
        item_id = item.id

    response = client.post(
        f"/api/v1/reimbursements/claims/{claim_id}/standard-adjustment",
        json={
            "risks": [
                {
                    "risk_id": "risk-hotel-endpoint-1",
                    "item_id": item_id,
                    "title": "住宿超标待说明",
                    "risk": "住宿票据金额超过职级标准。",
                    "application_days": 99,
                    "original_amount": "1.00",
                    "reimbursable_amount": "999999.00",
                }
            ],
            "request_id": "standard-adjustment-endpoint-1",
        },
        headers={"x-auth-username": "emp-1", "x-auth-name": "Zhang San", "x-auth-grade": "P4"},
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["amount"] == "450.00"
    standard_flag = next(
        flag
        for flag in payload["risk_flags_json"]
        if isinstance(flag, dict) and flag.get("source") == "reimbursement_standard_adjustment"
    )
    assert standard_flag["original_amount"] == "1000.00"
    assert standard_flag["reimbursable_amount"] == "450.00"
    assert standard_flag["employee_absorbed_amount"] == "550.00"
    assert standard_flag["calculation_source"] == "server_policy"
    assert standard_flag["policy_days"] == 1
    assert standard_flag["policy_grade"] == "P4"
    assert standard_flag["policy_matched_city"] == "北京"
    assert standard_flag["policy_hotel_rate"] == "450.00"
    assert standard_flag["policy_rule_name"]
    assert standard_flag["policy_rule_version"]
    assert standard_flag["visibility_scope"] == "leader"


def test_claim_item_attachment_upload_preview_and_delete(monkeypatch, tmp_path) -> None:
    def fake_recognize(
        self,
        files: list[tuple[str, bytes, str | None]],
    ) -> OcrRecognizeBatchRead:
        assert files[0][0] == "office-note.png"
        return OcrRecognizeBatchRead(
            total_file_count=1,
            success_count=1,
            documents=[
                OcrRecognizeDocumentRead(
                    filename="office-note.png",
                    media_type="image/png",
                    text="办公用品发票 金额88元 2026-05-13",
                    summary="识别到办公用品发票，金额 88 元。",
                    avg_score=0.98,
                    line_count=1,
                    page_count=1,
                    warnings=[],
                )
            ],
        )

    monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim_id = claim.id
        item_id = item.id

    headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
    file_bytes = b"fake-image-bytes"

    upload_response = client.post(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
        headers=headers,
        files=[("file", ("office-note.png", file_bytes, "image/png"))],
    )

    assert upload_response.status_code == 200
    upload_payload = upload_response.json()
    assert upload_payload["attachment"]["file_name"] == "office-note.png"
    assert upload_payload["attachment"]["analysis"]["label"] == "AI提示符合条件"
    assert upload_payload["attachment"]["document_info"]["document_type"] == "office_invoice"
    assert upload_payload["attachment"]["requirement_check"]["matches"] is True
    assert upload_payload["invoice_id"]
    assert upload_payload["item_type"] == "office"
    assert upload_payload["item_reason"] == "识别到办公用品发票，金额 88 元。"
    assert upload_payload["item_location"] == "深圳南山"
    assert upload_payload["item_date"] == "2026-05-13"
    assert upload_payload["item_amount"] == "88.00"

    meta_response = client.get(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/meta",
        headers=headers,
    )
    assert meta_response.status_code == 200
    meta_payload = meta_response.json()
    assert meta_payload["media_type"] == "image/png"
    assert meta_payload["preview_kind"] == "image"
    assert meta_payload["preview_url"].endswith(
        f"/reimbursements/claims/{claim_id}/items/{item_id}/attachment/preview"
    )
    assert meta_payload["analysis"]["headline"]
    assert meta_payload["document_info"]["fields"][0]["label"] == "金额"

    preview_response = client.get(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/preview",
        headers=headers,
    )
    assert preview_response.status_code == 200
    assert preview_response.content == file_bytes

    content_response = client.get(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
        headers=headers,
    )
    assert content_response.status_code == 200
    assert content_response.content == file_bytes

    delete_response = client.delete(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
        headers=headers,
    )
    assert delete_response.status_code == 200
    assert delete_response.json()["invoice_id"] is None

    deleted_meta_response = client.get(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/meta",
        headers=headers,
    )
    assert deleted_meta_response.status_code == 404


def test_claim_item_attachment_upload_flags_purpose_and_amount_mismatch(
    monkeypatch, tmp_path
) -> None:
    def fake_recognize(
        self,
        files: list[tuple[str, bytes, str | None]],
    ) -> OcrRecognizeBatchRead:
        return OcrRecognizeBatchRead(
            total_file_count=1,
            success_count=1,
            documents=[
                OcrRecognizeDocumentRead(
                    filename="taxi-note.png",
                    media_type="image/png",
                    text="滴滴出行电子发票 金额120元 2026-05-13",
                    summary="识别到交通出行发票，金额 120 元。",
                    avg_score=0.97,
                    line_count=1,
                    page_count=1,
                    warnings=[],
                )
            ],
        )

    monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim_id = claim.id
        item_id = item.id

    headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
    upload_response = client.post(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
        headers=headers,
        files=[("file", ("taxi-note.png", b"fake-image-bytes", "image/png"))],
    )

    assert upload_response.status_code == 200
    analysis = upload_response.json()["attachment"]["analysis"]
    assert analysis["severity"] == "high"
    assert any("金额字段" in point for point in analysis["points"])
    assert any("附件类型要求" in point for point in analysis["points"])
    assert upload_response.json()["attachment"]["requirement_check"]["matches"] is False


def test_claim_item_attachment_upload_flags_non_invoice_image_as_high_risk(
    monkeypatch, tmp_path
) -> None:
    def fake_recognize(
        self,
        files: list[tuple[str, bytes, str | None]],
    ) -> OcrRecognizeBatchRead:
        return OcrRecognizeBatchRead(
            total_file_count=1,
            success_count=1,
            documents=[
                OcrRecognizeDocumentRead(
                    filename="random-image.png",
                    media_type="image/png",
                    text="",
                    summary="",
                    avg_score=0.0,
                    line_count=0,
                    page_count=1,
                    warnings=[],
                )
            ],
        )

    monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim_id = claim.id
        item_id = item.id

    headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
    upload_response = client.post(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
        headers=headers,
        files=[("file", ("random-image.png", b"fake-image-bytes", "image/png"))],
    )

    assert upload_response.status_code == 200
    analysis = upload_response.json()["attachment"]["analysis"]
    assert analysis["severity"] == "high"
    assert any("附件内容" in point for point in analysis["points"])


def test_approve_claim_endpoint_routes_direct_manager_claim_to_finance_review() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        manager = Employee(
            id="mgr-approve-1",
            employee_no="E21001",
            name="李经理",
            email="manager-approve-api@example.com",
        )
        employee = Employee(
            id="emp-approve-1",
            employee_no="E11001",
            name="张三",
            email="zhangsan-approve-api@example.com",
            manager=manager,
        )
        claim = ExpenseClaim(
            id="claim-approve-1",
            claim_no="EXP-APP-API-001",
            employee_id=employee.id,
            employee_name="张三",
            department_id="dept-1",
            department_name="市场部",
            project_code=None,
            expense_type="transport",
            reason="交通报销",
            location="上海",
            amount=Decimal("88.00"),
            currency="CNY",
            invoice_count=1,
            occurred_at=datetime(2026, 5, 13, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 13, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage="直属领导审批",
            risk_flags_json=[],
        )
        db.add_all([manager, employee, claim])
        db.commit()

    action_headers = {
        "X-Auth-Username": "manager-approve-api@example.com",
        "X-Auth-Name": "manager-approve-api@example.com",
        "X-Auth-Role-Codes": "manager",
    }
    stale_response = client.post(
        "/api/v1/reimbursements/claims/claim-approve-1/approve",
        json={
            "opinion": "情况属实，同意报销。",
            "request_id": "approve-api-stale-1",
            "expected_status": "draft",
            "expected_approval_stage": "直属领导审批",
        },
        headers=action_headers,
    )
    assert stale_response.status_code == 409

    response = client.post(
        "/api/v1/reimbursements/claims/claim-approve-1/approve",
        json={
            "opinion": "情况属实，同意报销。",
            "request_id": "approve-api-claim-1",
            "expected_status": "submitted",
            "expected_approval_stage": "直属领导审批",
        },
        headers=action_headers,
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["status"] == "submitted"
    assert payload["approval_stage"] == "财务审批"
    assert any(
        item["source"] == "manual_approval"
        and item["opinion"] == "情况属实，同意报销。"
        and item["operator"] == "李经理"
        and item["next_approval_stage"] == "财务审批"
        for item in payload["risk_flags_json"]
    )
    approval_events = [
        item for item in payload["risk_flags_json"] if item["source"] == "manual_approval"
    ]
    assert approval_events[0]["operator"] == "李经理"
    assert "manager-approve-api@example.com" not in approval_events[0]["message"]

    with session_factory() as db:
        moved_forward = db.get(ExpenseClaim, "claim-approve-1")
        assert moved_forward is not None
        moved_forward.status = "pending_payment"
        moved_forward.approval_stage = "待付款"
        moved_forward.risk_flags_json = [
            *list(moved_forward.risk_flags_json or []),
            {"source": "future_state", "message": "首次响应之后的状态"},
        ]
        db.commit()

    replay_response = client.post(
        "/api/v1/reimbursements/claims/claim-approve-1/approve",
        json={
            "opinion": "情况属实，同意报销。",
            "request_id": "approve-api-claim-1",
            "expected_status": "submitted",
            "expected_approval_stage": "直属领导审批",
        },
        headers=action_headers,
    )
    assert replay_response.status_code == 200
    assert replay_response.json() == payload
    assert all(
        item.get("source") != "future_state"
        for item in replay_response.json()["risk_flags_json"]
    )

    changed_payload_response = client.post(
        "/api/v1/reimbursements/claims/claim-approve-1/approve",
        json={
            "opinion": "改为有条件通过",
            "request_id": "approve-api-claim-1",
            "expected_status": "submitted",
            "expected_approval_stage": "直属领导审批",
        },
        headers=action_headers,
    )
    assert changed_payload_response.status_code == 409
    with session_factory() as db:
        ledgers = list(db.scalars(select(ApprovalActionLedger)).all())
        assert len(ledgers) == 1
        assert ledgers[0].completed_at is not None


def test_claim_owner_cannot_discover_or_operate_manager_approval_task() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        superior = Employee(
            id="manager-self-approval-superior",
            employee_no="E-SELF-APPROVAL-SUPERIOR",
            name="王总",
            email="superior-self-approval-api@example.com",
        )
        manager = Employee(
            id="manager-self-approval-owner",
            employee_no="E-SELF-APPROVAL-OWNER",
            name="李经理",
            email="manager-self-approval-api@example.com",
            manager=superior,
        )
        claim = ExpenseClaim(
            id="claim-self-approval-api",
            claim_no="EXP-SELF-APPROVAL-API",
            employee_id=manager.id,
            employee_name=manager.name,
            department_name="市场部",
            expense_type="transport",
            reason="交通报销",
            location="上海",
            amount=Decimal("66.00"),
            currency="CNY",
            invoice_count=1,
            occurred_at=datetime(2026, 5, 13, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 13, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage="直属领导审批",
            risk_flags_json=[],
        )
        db.add_all([superior, manager, claim])
        db.commit()

    headers = {
        "X-Auth-Username": "manager-self-approval-api@example.com",
        "X-Auth-Name": "manager-self-approval-api@example.com",
        "X-Auth-Role-Codes": "manager",
    }
    approve_response = client.post(
        "/api/v1/reimbursements/claims/claim-self-approval-api/approve",
        json={
            "opinion": "同意",
            "request_id": "claim-self-approval-approve",
            "expected_status": "submitted",
            "expected_approval_stage": "直属领导审批",
        },
        headers=headers,
    )
    return_response = client.post(
        "/api/v1/reimbursements/claims/claim-self-approval-api/return",
        json={
            "reason": "退回",
            "request_id": "claim-self-approval-return",
            "expected_status": "submitted",
            "expected_approval_stage": "直属领导审批",
        },
        headers=headers,
    )

    assert approve_response.status_code == 404
    assert return_response.status_code == 404
    assert approve_response.json()["detail"] == "Approval task not found."
    assert return_response.json()["detail"] == "Approval task not found."
    with session_factory() as db:
        claim = db.get(ExpenseClaim, "claim-self-approval-api")
        assert claim is not None
        assert claim.status == "submitted"
        assert claim.approval_stage == "直属领导审批"
        assert claim.risk_flags_json == []
        assert db.scalar(select(func.count(ApprovalActionLedger.id))) == 0


def test_approve_claim_endpoint_blocks_open_high_risk_with_machine_readable_detail() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        manager = Employee(
            id="manager-risk-block-api",
            employee_no="M-RISK-BLOCK-API",
            name="风险经理",
            email="manager-risk-block-api@example.com",
        )
        employee = Employee(
            id="employee-risk-block-api",
            employee_no="E-RISK-BLOCK-API",
            name="风险员工",
            email="employee-risk-block-api@example.com",
            manager=manager,
        )
        claim = ExpenseClaim(
            id="claim-risk-block-api",
            claim_no="EXP-RISK-BLOCK-API",
            employee=employee,
            employee_name=employee.name,
            department_name="风控部",
            expense_type="travel",
            reason="客户拜访",
            location="上海",
            amount=Decimal("1200.00"),
            currency="CNY",
            invoice_count=1,
            occurred_at=datetime(2026, 7, 16, tzinfo=UTC),
            submitted_at=datetime(2026, 7, 16, tzinfo=UTC),
            status="submitted",
            approval_stage="直属领导审批",
            risk_flags_json=[],
        )
        observation = RiskObservation(
            id="risk-block-api-observation",
            tenant_id="default",
            observation_key="risk:claim-risk-block-api:duplicate",
            subject_type="expense_claim",
            subject_key="claim:claim-risk-block-api",
            subject_label=claim.claim_no,
            claim_id=claim.id,
            claim_no=claim.claim_no,
            risk_type="duplicate_invoice",
            risk_signal="duplicate_invoice",
            title="重复票据风险",
            description="同一票据可能重复报销。",
            risk_score=92,
            risk_level="high",
            confidence_score=0.95,
            control_stage="reimbursement",
            control_mode="risk_observation",
            automation_mode="semi_auto_review",
            source="financial_risk_graph",
            algorithm_version="financial_risk_graph.v1",
            status="pending_review",
            feedback_status="unreviewed",
        )
        db.add_all([manager, employee, claim, observation])
        db.commit()
        manager_email = manager.email

    response = client.post(
        "/api/v1/reimbursements/claims/claim-risk-block-api/approve",
        headers={
            "X-Auth-Username": manager_email,
            "X-Auth-Name": "Risk Manager",
            "X-Auth-Role-Codes": "manager",
        },
        json={
            "opinion": "同意",
            "request_id": "approve-risk-block-api-001",
            "expected_status": "submitted",
            "expected_approval_stage": "直属领导审批",
        },
    )

    assert response.status_code == 409
    detail = response.json()["detail"]
    assert detail["code"] == "APPROVAL_BLOCKED_BY_OPEN_HIGH_RISK"
    assert detail["observations"] == [
        {
            "id": "risk-block-api-observation",
            "title": "重复票据风险",
            "risk_level": "high",
            "adjudication": "unreviewed",
            "lifecycle_status": "open",
        }
    ]
    with session_factory() as db:
        persisted = db.get(ExpenseClaim, "claim-risk-block-api")
        assert persisted is not None
        assert persisted.approval_stage == "直属领导审批"
        assert db.scalar(select(func.count()).select_from(ApprovalActionLedger)) == 0


def test_approve_application_endpoint_routes_direct_manager_review_to_budget_review() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        department = OrganizationUnit(
            id="dept-1",
            unit_code="DELIVERY-API",
            name="交付部",
            unit_type="department",
        )
        budget_role = Role(
            id="role-budget-application-approve-1",
            role_code="budget_monitor",
            name="预算监控员",
        )
        manager = Employee(
            id="mgr-application-approve-1",
            employee_no="E21002",
            name="李经理",
            email="manager-application-approve-api@example.com",
            organization_unit=department,
        )
        budget_manager = Employee(
            id="budget-application-approve-1",
            employee_no="E31002",
            name="赵预算",
            email="budget-application-approve-api@example.com",
            grade="P8",
            organization_unit=department,
            roles=[budget_role],
        )
        employee = Employee(
            id="emp-application-approve-1",
            employee_no="E11002",
            name="张三",
            email="zhangsan-application-approve-api@example.com",
            manager=manager,
            organization_unit=department,
        )
        claim = ExpenseClaim(
            id="claim-application-approve-1",
            claim_no="APP-20260525-API001",
            employee_id=employee.id,
            employee_name="张三",
            department_id="dept-1",
            department_name="交付部",
            project_code=None,
            expense_type="travel_application",
            reason="支撑国网服务器上线部署",
            location="上海",
            amount=Decimal("12000.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 25, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage="直属领导审批",
            risk_flags_json=[
                    {
                        "source": "submission_review",
                        "severity": "high",
                        "actionability": "route_review",
                        "label": "申请风险复核",
                    "message": "申请金额和行程安排需要预算管理者二次确认。",
                }
            ],
        )
        db.add_all([department, budget_role, manager, budget_manager, employee, claim])
        db.commit()

    response = client.post(
        "/api/v1/reimbursements/claims/claim-application-approve-1/approve",
        json={
            "opinion": "业务必要，同意申请。",
            "request_id": "approve-api-application-1",
            "expected_status": "submitted",
            "expected_approval_stage": "直属领导审批",
        },
        headers={
            "X-Auth-Username": "manager-application-approve-api@example.com",
            "X-Auth-Name": "manager-application-approve-api@example.com",
            "X-Auth-Role-Codes": "manager",
        },
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["status"] == "submitted"
    assert payload["approval_stage"] == "预算管理者审批"
    assert any(
        item["source"] == "manual_approval"
        and item["event_type"] == "expense_application_approval"
        and item["opinion"] == "业务必要，同意申请。"
        and item["operator"] == "李经理"
        and item["next_status"] == "submitted"
        and item["next_approval_stage"] == "预算管理者审批"
        and item["next_approver_name"] == "赵预算"
        for item in payload["risk_flags_json"]
    )


def test_claim_item_pdf_attachment_preview_returns_generated_image(monkeypatch, tmp_path) -> None:
    preview_bytes = b"fake-preview-png"
    preview_data_url = f"data:image/png;base64,{base64.b64encode(preview_bytes).decode('ascii')}"

    def fake_recognize(
        self,
        files: list[tuple[str, bytes, str | None]],
    ) -> OcrRecognizeBatchRead:
        return OcrRecognizeBatchRead(
            total_file_count=1,
            success_count=1,
            documents=[
                OcrRecognizeDocumentRead(
                    filename="invoice.pdf",
                    media_type="application/pdf",
                    text="滴滴出行电子发票 金额13.4元",
                    summary="识别到交通票据，金额 13.4 元。",
                    avg_score=0.96,
                    line_count=1,
                    page_count=1,
                    document_type="taxi_receipt",
                    document_type_label="出租车/网约车票据",
                    scene_code="transport",
                    scene_label="交通票据",
                    preview_kind="image",
                    preview_data_url=preview_data_url,
                    warnings=[],
                )
            ],
        )

    monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim_id = claim.id
        item_id = item.id

    headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
    upload_response = client.post(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
        headers=headers,
        files=[("file", ("invoice.pdf", b"%PDF-1.4 fake", "application/pdf"))],
    )

    assert upload_response.status_code == 200
    meta_payload = upload_response.json()["attachment"]
    assert meta_payload["preview_kind"] == "image"
    assert meta_payload["preview_url"].endswith(
        f"/reimbursements/claims/{claim_id}/items/{item_id}/attachment/preview"
    )
    meta_path = next(tmp_path.rglob("invoice.pdf.meta.json"))
    stored_meta = json.loads(meta_path.read_text(encoding="utf-8"))
    assert stored_meta["preview_rendered_with"] == DocumentPreviewAssets.PDF_RENDERER_ID

    preview_response = client.get(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/preview",
        headers=headers,
    )
    assert preview_response.status_code == 200
    assert preview_response.headers["content-type"].startswith("image/png")
    assert preview_response.content == preview_bytes


def test_claim_item_delete_removes_item_and_attachment(monkeypatch, tmp_path) -> None:
    def fake_recognize(
        self,
        files: list[tuple[str, bytes, str | None]],
    ) -> OcrRecognizeBatchRead:
        return OcrRecognizeBatchRead(
            total_file_count=1,
            success_count=1,
            documents=[
                OcrRecognizeDocumentRead(
                    filename="office-note.png",
                    media_type="image/png",
                    text="办公用品发票 金额88元 2026-05-13",
                    summary="识别到办公用品发票，金额 88 元。",
                    avg_score=0.98,
                    line_count=1,
                    page_count=1,
                    warnings=[],
                )
            ],
        )

    monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        claim, item = seed_claim(db)
        claim_id = claim.id
        item_id = item.id

    headers = {"x-auth-username": "emp-1", "x-auth-name": "Zhang San"}
    upload_response = client.post(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment",
        headers=headers,
        files=[("file", ("office-note.png", b"fake-image-bytes", "image/png"))],
    )
    assert upload_response.status_code == 200
    assert (tmp_path / claim_id / item_id).exists()

    delete_response = client.delete(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}",
        headers=headers,
    )
    assert delete_response.status_code == 200
    assert delete_response.json()["item_id"] == item_id
    assert not (tmp_path / claim_id / item_id).exists()

    detail_response = client.get(
        f"/api/v1/reimbursements/claims/{claim_id}",
        headers=headers,
    )
    assert detail_response.status_code == 200
    detail_payload = detail_response.json()
    assert detail_payload["items"] == []
    assert detail_payload["invoice_count"] == 0
    assert detail_payload["employee_position"] == "招商主管"
    assert detail_payload["employee_grade"] == "P4"
    assert detail_payload["manager_name"] == "李总"
    assert detail_payload["role_labels"] == ["员工"]

    deleted_meta_response = client.get(
        f"/api/v1/reimbursements/claims/{claim_id}/items/{item_id}/attachment/meta",
        headers=headers,
    )
    assert deleted_meta_response.status_code == 404


def test_claim_delete_allows_admin_and_cleans_risk_observations(monkeypatch, tmp_path) -> None:
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        claim, _ = seed_claim(db)
        observation = RiskObservation(
            id="risk-observation-delete-1",
            observation_key="claim-delete-risk-observation-1",
            subject_type="expense_claim",
            subject_key=claim.id,
            subject_label=claim.claim_no,
            claim_id=claim.id,
            claim_no=claim.claim_no,
            risk_type="policy",
            risk_signal="draft_pre_review",
            title="草稿预审风险",
            description="删除草稿时应同步清理关联风险观察。",
            risk_score=70,
            risk_level="medium",
            confidence_score=0.8,
        )
        feedback = RiskObservationFeedback(
            id="risk-observation-feedback-delete-1",
            observation=observation,
            feedback_type="confirm",
            actor="auditor",
        )
        db.add(observation)
        db.add(feedback)
        db.commit()
        claim_id = claim.id

    response = client.delete(
        f"/api/v1/reimbursements/claims/{claim_id}",
        headers={"x-auth-username": "admin", "x-auth-name": "Admin User"},
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["claim_id"] == claim_id
    assert payload["status"] == "deleted"

    with session_factory() as db:
        assert db.get(ExpenseClaim, claim_id) is None
        assert db.get(RiskObservation, "risk-observation-delete-1") is None
        assert db.get(RiskObservationFeedback, "risk-observation-feedback-delete-1") is None


def test_claim_delete_allows_applicant_to_delete_own_draft(monkeypatch, tmp_path) -> None:
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        claim, _ = seed_claim(db)
        claim.claim_no = "AP-20260620-DRAFT"
        claim.expense_type = "travel_application"
        claim_id = claim.id
        db.commit()

    response = client.delete(
        f"/api/v1/reimbursements/claims/{claim_id}",
        headers={
            "x-auth-username": "zhangsan@example.com",
            "x-auth-name": "Zhang San",
            "x-auth-employee-no": "E10001",
            "x-auth-role-codes": "user",
        },
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["claim_id"] == claim_id
    assert payload["status"] == "deleted"
    assert "申请单已删除" in payload["message"]

    with session_factory() as db:
        assert db.get(ExpenseClaim, claim_id) is None


def test_claim_delete_allows_legacy_superadmin_without_is_admin_header(
    monkeypatch, tmp_path
) -> None:
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        claim, _ = seed_claim(db)
        claim_id = claim.id

    response = client.delete(
        f"/api/v1/reimbursements/claims/{claim_id}",
        headers={
            "x-auth-username": "superadmin",
            "x-auth-name": "superadmin",
            "x-auth-role-codes": "manager",
        },
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["claim_id"] == claim_id
    assert payload["status"] == "deleted"

    with session_factory() as db:
        assert db.get(ExpenseClaim, claim_id) is None


def test_application_preview_action_submits_without_orchestrator_run(monkeypatch, tmp_path) -> None:
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        seed_claim(db)

    response = client.post(
        "/api/v1/reimbursements/application-preview-action",
        headers={
            "x-auth-username": "zhangsan@example.com",
            "x-auth-name": "Zhang San",
            "x-auth-employee-no": "E10001",
            "x-auth-role-codes": "user",
            "x-auth-department": "Marketing",
        },
        json={
            "source": "user_message",
            "user_id": "zhangsan@example.com",
            "conversation_id": "conversation-fast-submit",
            "message": "\n".join(
                [
                    "差旅费用申请提交审批",
                    "申请类型：差旅费用申请",
                    "申请时间：2026-07-01 至 2026-07-03",
                    "地点：北京",
                    "事由：项目实施",
                    "天数：3天",
                    "出行方式：火车",
                    "申请金额：1000元",
                    "直接提交",
                ]
            ),
            "context_json": {
                "session_type": "application",
                "entry_source": "workbench_ai_inline",
                "document_type": "expense_application",
                "application_stage": "expense_application",
                "application_preview": {
                    "fields": {
                        "applicationType": "差旅费用申请",
                        "time": "2026-07-01 至 2026-07-03",
                        "location": "北京",
                        "reason": "项目实施",
                        "days": "3天",
                        "transportMode": "火车",
                        "amount": "1000元",
                        "applicant": "张三",
                        "department": "市场部",
                        "position": "招商主管",
                        "grade": "P4",
                        "managerName": "李总",
                    }
                },
            },
        },
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["status"] == "succeeded"
    draft_payload = payload["result"]["draft_payload"]
    assert draft_payload["draft_type"] == "expense_application"
    assert draft_payload["status"] == "submitted"
    assert draft_payload["approval_stage"] == "直属领导审批"
    assert draft_payload["claim_no"].startswith("A")

    with session_factory() as db:
        claim = db.get(ExpenseClaim, draft_payload["claim_id"])
        assert claim is not None
        assert claim.status == "submitted"
        assert claim.employee_name == "张三"
        reservation = db.scalar(
            select(BudgetReservation).where(
                BudgetReservation.source_type == "application",
                BudgetReservation.source_id == claim.id,
                BudgetReservation.source_status == "active",
            )
        )
        assert reservation is not None
        assert reservation.amount == Decimal("1000.00")
        case_link = db.scalar(
            select(ExpenseCaseLink).where(
                ExpenseCaseLink.resource_type == "expense_claim",
                ExpenseCaseLink.resource_id == claim.id,
            )
        )
        assert case_link is not None
        event = db.scalar(
            select(BusinessEvent).where(
                BusinessEvent.aggregate_id == claim.id,
                BusinessEvent.event_type == "application_submitted",
            )
        )
        assert event is not None
        assert event.expense_case_id == case_link.expense_case_id
        assert event.payload_json["previous_status"] == "draft"
        assert event.payload_json["next_status"] == "submitted"
        decision = db.scalar(select(AIDecision).where(AIDecision.expense_claim_id == claim.id))
        feedback = db.scalar(
            select(AIDecisionFeedback).where(AIDecisionFeedback.expense_claim_id == claim.id)
        )
        outcome = db.scalar(
            select(WorkflowOutcome).where(WorkflowOutcome.expense_claim_id == claim.id)
        )
        assert decision is not None
        assert decision.expense_case_id == case_link.expense_case_id
        assert decision.correlation_id == event.correlation_id
        assert decision.business_event_id == event.id
        assert feedback is not None
        assert feedback.feedback_type == "accepted"
        assert feedback.action_type == "submit"
        assert outcome is not None
        assert outcome.outcome_type == "application_submitted"
        assert outcome.business_event_id == event.id
        assert any(
            isinstance(flag, dict) and flag.get("event_type") == "expense_application_submission"
            for flag in list(claim.risk_flags_json or [])
        )


def test_application_direct_submit_rolls_back_budget_when_case_event_fails(
    monkeypatch,
) -> None:
    session_factory = build_session_factory()

    def fail_event(*_args, **_kwargs):
        raise RuntimeError("模拟费用事件写入失败")

    monkeypatch.setattr(ExpenseCaseService, "record_claim_event", fail_event)
    request = UserAgentRequest(
        run_id="application-submit-rollback",
        user_id="rollback@example.com",
        message="直接提交",
        ontology=OntologyParseResult(run_id="application-submit-rollback"),
        context_json={
            "session_type": "application",
            "name": "回滚测试用户",
            "department_name": "Test Department",
        },
    )
    facts = {
        "application_type": "差旅费用申请",
        "time": "2026-07-01 至 2026-07-03",
        "location": "北京",
        "reason": "验证提交失败回滚",
        "amount": "1000元",
    }

    with session_factory() as db:
        with pytest.raises(RuntimeError, match="模拟费用事件写入失败"):
            UserAgentService(db)._create_expense_application_record(
                request,
                facts,
                submit=True,
            )

        # 模拟外层捕获异常后继续记录工具日志并提交，失败草稿仍不能被顺带落库。
        db.commit()
        assert list(db.scalars(select(ExpenseClaim)).all()) == []
        assert list(db.scalars(select(BudgetAllocation)).all()) == []
        assert list(db.scalars(select(BudgetReservation)).all()) == []
        assert list(db.scalars(select(BudgetTransaction)).all()) == []
        assert list(db.scalars(select(ExpenseCaseLink)).all()) == []
        assert list(db.scalars(select(BusinessEvent)).all()) == []
        assert list(db.scalars(select(AIDecision)).all()) == []
        assert list(db.scalars(select(AIDecisionFeedback)).all()) == []
        assert list(db.scalars(select(WorkflowOutcome)).all()) == []


def test_application_preview_action_saves_draft_with_detail_reference(
    monkeypatch, tmp_path
) -> None:
    monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: tmp_path)

    client, session_factory = build_client()
    with session_factory() as db:
        seed_claim(db)

    response = client.post(
        "/api/v1/reimbursements/application-preview-action",
        headers={
            "x-auth-username": "zhangsan@example.com",
            "x-auth-name": "Zhang San",
            "x-auth-employee-no": "E10001",
            "x-auth-role-codes": "user",
        },
        json={
            "source": "user_message",
            "user_id": "zhangsan@example.com",
            "conversation_id": "conversation-fast-save",
            "message": "\n".join(
                [
                    "费用申请保存草稿",
                    "申请类型：差旅费用申请",
                    "申请时间：2026-07-04 至 2026-07-05",
                    "地点：上海",
                    "事由：项目验收",
                    "天数：2天",
                    "出行方式：火车",
                    "申请金额：800元",
                    "保存草稿",
                ]
            ),
            "context_json": {
                "session_type": "application",
                "entry_source": "workbench_ai_inline",
                "document_type": "expense_application",
                "application_stage": "expense_application",
                "application_action": "save_draft",
                "application_save_mode": True,
                "application_preview": {
                    "fields": {
                        "applicationType": "差旅费用申请",
                        "time": "2026-07-04 至 2026-07-05",
                        "location": "上海",
                        "reason": "项目验收",
                        "days": "2天",
                        "transportMode": "火车",
                        "amount": "800元",
                        "applicant": "张三",
                        "department": "市场部",
                        "position": "招商主管",
                        "grade": "P4",
                        "managerName": "李总",
                    }
                },
            },
        },
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["status"] == "succeeded"
    draft_payload = payload["result"]["draft_payload"]
    assert draft_payload["draft_type"] == "expense_application"
    assert draft_payload["status"] == "draft"
    assert draft_payload["approval_stage"] == "待提交"
    assert draft_payload["claim_id"]
    assert draft_payload["claim_no"].startswith("A")

    with session_factory() as db:
        claim = db.get(ExpenseClaim, draft_payload["claim_id"])
        assert claim is not None
        assert claim.status == "draft"
        assert claim.approval_stage == "待提交"
        assert claim.submitted_at is None
        assert claim.employee_name == "张三"
        assert (
            db.scalar(select(BudgetReservation).where(BudgetReservation.source_id == claim.id))
            is None
        )
        event = db.scalar(select(BusinessEvent).where(BusinessEvent.aggregate_id == claim.id))
        assert event is not None
        assert event.event_type == "claim_draft_created"
        assert event.payload_json["previous_status"] == ""
        assert event.payload_json["next_status"] == "draft"
        link = db.scalar(select(ExpenseCaseLink).where(ExpenseCaseLink.resource_id == claim.id))
        assert link is not None
        assert link.relation_type == "application"


def test_application_preview_action_rejects_forged_identity_when_editing_other_claim() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        seed_claim(db)
        outsider = Employee(
            id="emp-outsider",
            employee_no="E90001",
            name="其他员工",
            email="outsider@example.com",
        )
        outsider_claim = ExpenseClaim(
            id="application-outsider-1",
            claim_no="AP-OUTSIDER-001",
            employee_id=outsider.id,
            employee_name=outsider.name,
            department_name="交付部",
            expense_type="travel_application",
            reason="其他员工原申请",
            location="北京",
            amount=Decimal("500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 7, 10, tzinfo=UTC),
            submitted_at=None,
            status="returned",
            approval_stage="退回补充",
            risk_flags_json=[],
        )
        db.add_all([outsider, outsider_claim])
        db.commit()

    response = client.post(
        "/api/v1/reimbursements/application-preview-action",
        headers={
            "x-auth-username": "zhangsan@example.com",
            "x-auth-name": "Zhang San",
            "x-auth-employee-no": "E10001",
            "x-auth-role-codes": "user",
        },
        json={
            "source": "user_message",
            "user_id": "outsider@example.com",
            "conversation_id": "conversation-forged-identity",
            "message": "费用申请保存草稿\n申请时间：2026-07-13 至 2026-07-14\n地点：上海\n事由：恶意修改\n申请金额：880元\n保存草稿",
            "context_json": {
                "session_type": "application",
                "application_action": "save_draft",
                "application_save_mode": True,
                "application_edit_mode": True,
                "application_edit_claim_id": "application-outsider-1",
                "username": "outsider@example.com",
                "name": "其他员工",
                "employee_no": "E90001",
                "role_codes": ["admin"],
                "is_admin": True,
            },
        },
    )

    assert response.status_code == 400
    assert response.json()["detail"] == "只能修改本人被退回的申请单。"
    with session_factory() as db:
        persisted = db.get(ExpenseClaim, "application-outsider-1")
        assert persisted is not None
        assert persisted.reason == "其他员工原申请"
        assert persisted.status == "returned"
        assert persisted.approval_stage == "退回补充"
        assert (
            db.scalar(select(BusinessEvent).where(BusinessEvent.aggregate_id == persisted.id))
            is None
        )


def test_application_preview_action_reuses_created_draft_for_identical_retry() -> None:
    client, session_factory = build_client()
    with session_factory() as db:
        seed_claim(db)

    request_payload = {
        "source": "user_message",
        "user_id": "zhangsan@example.com",
        "conversation_id": "conversation-fast-save-retry",
        "message": "费用申请保存草稿\n地点：上海\n事由：项目验收\n申请金额：880元\n保存草稿",
        "context_json": {
            "session_type": "application",
            "application_action": "save_draft",
            "application_save_mode": True,
        },
    }
    headers = {
        "x-auth-username": "zhangsan@example.com",
        "x-auth-name": "Zhang San",
        "x-auth-employee-no": "E10001",
        "x-auth-role-codes": "user",
    }

    first_response = client.post(
        "/api/v1/reimbursements/application-preview-action",
        headers=headers,
        json=request_payload,
    )
    second_response = client.post(
        "/api/v1/reimbursements/application-preview-action",
        headers=headers,
        json=request_payload,
    )

    assert first_response.status_code == 200
    assert second_response.status_code == 200
    first_draft = first_response.json()["result"]["draft_payload"]
    second_draft = second_response.json()["result"]["draft_payload"]
    assert second_draft["claim_id"] == first_draft["claim_id"]
    assert second_draft["claim_no"] == first_draft["claim_no"]
    with session_factory() as db:
        application_claims = list(
            db.scalars(
                select(ExpenseClaim).where(ExpenseClaim.expense_type == "travel_application")
            ).all()
        )
        assert len(application_claims) == 1
        events = list(
            db.scalars(
                select(BusinessEvent).where(BusinessEvent.aggregate_id == application_claims[0].id)
            ).all()
        )
        assert len(events) == 1
        assert events[0].event_type == "claim_draft_created"
