from __future__ import annotations

from collections.abc import Generator
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from types import SimpleNamespace

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

from app.algorithem.risk_graph.replay import AlgorithmReplaySetBuilder
from app.api.deps import get_db
from app.api.v1.endpoints.risk_observations import router as risk_observations_router
from app.db.base import Base
from app.models.employee import Employee
from app.models.expense_case import ExpenseCase, ExpenseCaseLink
from app.models.financial_record import ExpenseClaim
from app.models.risk_observation import RiskObservation
from app.schemas.risk_observation import RiskObservationFeedbackCreate
from app.services.expense_claims import ExpenseClaimService
from app.services.hermes_risk_scanner import HermesRiskScannerService
from app.services.risk_observations import RiskObservationService


def test_risk_observation_service_upserts_and_summarizes_dashboard() -> None:
    with _build_session() as db:
        db.add(_employee_orm())
        db.add_all([_claim_orm("c1", "BX-001"), _claim_orm("c2", "BX-002")])
        db.flush()
        service = RiskObservationService(db)
        service.upsert_observation(_observation_payload("risk:c1:duplicate_invoice"))
        service.upsert_observation(
            {
                **_observation_payload("risk:c2:preapproval_absent"),
                "claim_id": "c2",
                "claim_no": "BX-002",
                "risk_signal": "preapproval_absent",
                "risk_type": "preapproval_absent",
                "risk_score": 72,
                "risk_level": "high",
            }
        )
        db.commit()

        feedback = service.create_feedback(
            "risk:c1:duplicate_invoice",
            RiskObservationFeedbackCreate(feedback_type="confirm", actor="auditor"),
        )
        dashboard = service.summarize_dashboard(window_days=30)
        history = service.build_history_stats(risk_signals={"duplicate_invoice"})
        refreshed = service.get_observation("risk:c1:duplicate_invoice")

        assert feedback.feedback_type == "confirm"
        assert refreshed is not None
        assert refreshed.status == "confirmed"
        assert refreshed.source == "financial_risk_graph"
        assert refreshed.algorithm_version == "financial_risk_graph.v1"
        assert refreshed.sampling_strategy["strategy"] == "focused_review"
        assert refreshed.evaluation_case_id == "case-duplicate-invoice"
        assert refreshed.ontology_parse_id == "parse-1"
        assert refreshed.ontology_version == "ontology.v1"
        assert refreshed.domain == "expense"
        assert refreshed.scenario == "reimbursement"
        assert refreshed.intent == "risk_check"
        assert refreshed.ontology_entities_json == [{"type": "claim", "value": "c1"}]
        assert refreshed.risk_signals_json == [{"code": "duplicate_invoice"}]
        assert refreshed.canonical_subject_key == "claim:c1"
        assert dashboard.total_observations == 2
        assert dashboard.high_or_above_count == 2
        assert dashboard.risk_clue_count == 1
        assert dashboard.confirmed_count == 1
        assert dashboard.feedback_sample_count == 1
        assert dashboard.total_amount == 2400.0
        assert dashboard.level_distribution["high"] == 2
        assert dashboard.signal_distribution["duplicate_invoice"] == 1
        assert dashboard.department_distribution["风控部"] == 2
        assert dashboard.expense_type_distribution["travel"] == 2
        assert dashboard.employee_grade_distribution["P6"] == 2
        assert dashboard.supplier_distribution["上海差旅供应商"] == 2
        assert dashboard.top_departments[0]["name"] == "风控部"
        assert dashboard.top_departments[0]["amount"] == 2400.0
        assert dashboard.top_employees[0]["name"] == "风险员工"
        assert dashboard.top_suppliers[0]["name"] == "上海差旅供应商"
        assert dashboard.top_expense_types[0]["name"] == "travel"
        assert dashboard.top_rules[0]["name"] == "policy.duplicate_invoice"
        assert dashboard.top_risk_signals[0]["name"] in {
            "duplicate_invoice",
            "preapproval_absent",
        }
        assert dashboard.daily_trend
        assert history[0].risk_signal == "duplicate_invoice"
        assert history[0].confirmed_count == 1


def test_platform_rule_flags_are_persisted_as_risk_observations() -> None:
    with _build_session() as db:
        claim = _claim_orm("c-platform", "BX-PLATFORM")
        db.add(claim)
        db.flush()

        observations = RiskObservationService(db).upsert_platform_risk_flags(
            claim,
            [
                {
                    "hit_source": "rule_center",
                    "rule_type": "risk",
                    "rule_code": "risk.invoice.duplicate_invoice",
                    "rule_version": "v1.2.0",
                    "severity": "critical",
                    "action": "block",
                    "label": "重复发票校验",
                    "message": "票据号码已在其他报销单中出现。",
                    "evidence": {"invoice_no": "INV-001"},
                }
            ],
        )
        db.commit()

        assert len(observations) == 1
        persisted = db.query(RiskObservation).filter_by(claim_id="c-platform").one()
        assert persisted.risk_signal == "duplicate_invoice"
        assert persisted.risk_level == "critical"
        assert persisted.source == "rule_center"
        assert persisted.algorithm_version == "v1.2.0"
        assert persisted.contribution_scores_json == {"S_rule": 100}


def test_high_platform_risk_persistence_failure_is_fail_closed(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    with _build_session() as db:
        claim = _claim_orm("c-platform-fail-closed", "BX-PLATFORM-FAIL-CLOSED")
        db.add(claim)
        db.flush()
        service = ExpenseClaimService(db)
        monkeypatch.setattr(
            service,
            "evaluate_platform_risk_rules",
            lambda _claim, **_kwargs: {
                "flags": [
                    {
                        "source": "platform_risk",
                        "hit_source": "rule_center",
                        "rule_type": "risk",
                        "rule_code": "risk.invoice.blocking",
                        "severity": "high",
                        "action": "block",
                        "label": "高风险票据",
                        "message": "票据需要人工核验。",
                    }
                ],
                "rule_set_fingerprint": "rules-v1",
            },
        )

        def fail_persistence(*_args, **_kwargs):
            raise RuntimeError("database unavailable")

        monkeypatch.setattr(
            RiskObservationService,
            "upsert_platform_risk_flags",
            fail_persistence,
        )

        with pytest.raises(RuntimeError, match="高风险观察持久化失败"):
            service._run_ai_submission_review(claim)


def test_risk_observation_storage_ready_is_cached_per_bind(monkeypatch: pytest.MonkeyPatch) -> None:
    with _build_session() as db:
        RiskObservationService._storage_ready_cache.clear()
        create_all_calls = []
        original_create_all = Base.metadata.create_all

        def spy_create_all(*args, **kwargs):
            create_all_calls.append(kwargs.get("bind"))
            return original_create_all(*args, **kwargs)

        monkeypatch.setattr(Base.metadata, "create_all", spy_create_all)

        service = RiskObservationService(db)
        service.ensure_storage_ready()
        service.ensure_storage_ready()
        RiskObservationService(db).ensure_storage_ready()

        assert len(create_all_calls) == 1
        RiskObservationService._storage_ready_cache.clear()


def test_risk_observation_endpoints_return_list_detail_dashboard_and_feedback() -> None:
    client, session_factory = _build_client()
    with session_factory() as db:
        service = RiskObservationService(db)
        service.upsert_observation(
            _observation_payload("risk:c1:duplicate_invoice"),
            execution_log_id="exec-1",
        )
        db.commit()

    list_response = client.get("/api/v1/risk-observations", params={"risk_level": "high"})
    execution_log_response = client.get("/api/v1/risk-observations/execution-log/exec-1")
    detail_response = client.get("/api/v1/risk-observations/risk:c1:duplicate_invoice")
    dashboard_response = client.get("/api/v1/risk-observations/dashboard")
    feedback_response = client.post(
        "/api/v1/risk-observations/risk:c1:duplicate_invoice/feedback",
        json={"feedback_type": "false_positive", "actor": "auditor", "comment": "误报"},
    )

    assert list_response.status_code == 200
    assert list_response.json()["total"] == 1
    assert execution_log_response.status_code == 200
    assert len(execution_log_response.json()) == 1
    assert detail_response.status_code == 200
    assert detail_response.json()["risk_signal"] == "duplicate_invoice"
    assert dashboard_response.status_code == 200
    assert dashboard_response.json()["total_observations"] == 1
    assert dashboard_response.json()["risk_clue_count"] == 1
    assert dashboard_response.json()["feedback_sample_count"] == 0
    assert "top_departments" in dashboard_response.json()
    assert feedback_response.status_code == 200
    assert feedback_response.json()["feedback_type"] == "false_positive"
    assert feedback_response.json()["actor"] == "Test Admin"

    updated_detail_response = client.get("/api/v1/risk-observations/risk:c1:duplicate_invoice")
    assert updated_detail_response.status_code == 200
    assert updated_detail_response.json()["feedback_items"][0]["feedback_type"] == "false_positive"

    with session_factory() as db:
        observation = (
            db.query(RiskObservation).filter_by(observation_key="risk:c1:duplicate_invoice").one()
        )
        assert observation.status == "false_positive"
        assert observation.feedback_status == "false_positive"


def test_risk_observation_endpoints_enforce_tenant_scope_and_authenticated_actor() -> None:
    client, session_factory = _build_client()
    with session_factory() as db:
        service = RiskObservationService(db)
        tenant_a = service.upsert_observation(
            {
                **_observation_payload("risk:tenant-a:duplicate_invoice"),
                "claim_id": "shared-claim",
            },
            tenant_id="tenant-a",
            execution_log_id="shared-execution-log",
        )
        tenant_b = service.upsert_observation(
            {
                **_observation_payload("risk:tenant-b:duplicate_invoice"),
                "claim_id": "shared-claim",
            },
            tenant_id="tenant-b",
            execution_log_id="shared-execution-log",
        )
        tenant_a_id = tenant_a.id
        tenant_b_id = tenant_b.id
        db.commit()

    tenant_a_headers = {
        "X-Auth-Username": "auditor-a",
        "X-Auth-Name": "Tenant A Auditor",
        "X-Auth-Role-Codes": "finance",
        "X-Auth-Is-Admin": "true",
        "X-Auth-Tenant-Id": "tenant-a",
    }
    tenant_b_headers = {
        "X-Auth-Username": "auditor-b",
        "X-Auth-Name": "Tenant B Auditor",
        "X-Auth-Role-Codes": "finance",
        "X-Auth-Is-Admin": "true",
        "X-Auth-Tenant-Id": "tenant-b",
    }

    list_response = client.get("/api/v1/risk-observations", headers=tenant_a_headers)
    detail_response = client.get(
        f"/api/v1/risk-observations/{tenant_a_id}",
        headers=tenant_a_headers,
    )
    foreign_detail_response = client.get(
        f"/api/v1/risk-observations/{tenant_b_id}",
        headers=tenant_a_headers,
    )
    claim_response = client.get(
        "/api/v1/risk-observations/claim/shared-claim",
        headers=tenant_a_headers,
    )
    execution_log_response = client.get(
        "/api/v1/risk-observations/execution-log/shared-execution-log",
        headers=tenant_a_headers,
    )
    dashboard_response = client.get(
        "/api/v1/risk-observations/dashboard",
        headers=tenant_a_headers,
    )
    foreign_feedback_response = client.post(
        f"/api/v1/risk-observations/{tenant_b_id}/feedback",
        headers=tenant_a_headers,
        json={"feedback_type": "confirm", "actor": "伪造管理员"},
    )
    own_feedback_response = client.post(
        f"/api/v1/risk-observations/{tenant_a_id}/feedback",
        headers=tenant_a_headers,
        json={"feedback_type": "confirm", "actor": "伪造管理员"},
    )

    assert list_response.status_code == 200
    assert list_response.json()["total"] == 1
    assert list_response.json()["items"][0]["tenant_id"] == "tenant-a"
    assert detail_response.status_code == 200
    assert detail_response.json()["tenant_id"] == "tenant-a"
    assert foreign_detail_response.status_code == 404
    # 单据风险入口必须先通过单据自身可见范围；不存在的历史 claim 不再旁路读取。
    assert claim_response.status_code == 404
    assert execution_log_response.status_code == 200
    assert [item["tenant_id"] for item in execution_log_response.json()] == ["tenant-a"]
    assert dashboard_response.status_code == 200
    assert dashboard_response.json()["total_observations"] == 1
    assert foreign_feedback_response.status_code == 404
    assert own_feedback_response.status_code == 200
    assert own_feedback_response.json()["actor"] == "Tenant A Auditor"

    tenant_b_detail = client.get(
        f"/api/v1/risk-observations/{tenant_b_id}",
        headers=tenant_b_headers,
    )
    assert tenant_b_detail.status_code == 200
    assert tenant_b_detail.json()["status"] == "pending_review"
    assert tenant_b_detail.json()["feedback_items"] == []


def test_risk_observation_service_scopes_history_and_same_key_upserts_by_tenant() -> None:
    with _build_session() as db:
        service = RiskObservationService(db)
        tenant_a = service.upsert_observation(
            _observation_payload("risk:shared:duplicate_invoice"),
            tenant_id="tenant-a",
        )
        tenant_b = service.upsert_observation(
            _observation_payload("risk:shared:duplicate_invoice"),
            tenant_id="tenant-b",
        )
        service.create_feedback(
            tenant_a.id,
            RiskObservationFeedbackCreate(feedback_type="confirm", actor="untrusted"),
            tenant_id="tenant-a",
            actor="trusted-auditor",
        )

        tenant_a_items, tenant_a_total = service.list_observations(tenant_id="tenant-a")
        tenant_b_items, tenant_b_total = service.list_observations(tenant_id="tenant-b")
        tenant_a_history = service.build_history_stats(
            tenant_id="tenant-a",
            risk_signals={"duplicate_invoice"},
        )
        tenant_b_history = service.build_history_stats(
            tenant_id="tenant-b",
            risk_signals={"duplicate_invoice"},
        )

        assert tenant_a.id != tenant_b.id
        assert tenant_a_total == tenant_b_total == 1
        assert [item.tenant_id for item in tenant_a_items] == ["tenant-a"]
        assert [item.tenant_id for item in tenant_b_items] == ["tenant-b"]
        assert tenant_a_history[0].confirmed_count == 1
        assert tenant_b_history[0].confirmed_count == 0
        assert tenant_a.feedback_items[0].actor == "trusted-auditor"
        assert service.get_observation(tenant_b.id, tenant_id="tenant-a") is None


def test_risk_observation_rejects_explicit_tenant_mismatching_claim_link() -> None:
    with _build_session() as db:
        claim = _claim_orm("claim-tenant-boundary", "BX-TENANT-BOUNDARY")
        expense_case = ExpenseCase(
            id="case-tenant-boundary",
            tenant_id="tenant-a",
            case_no="CASE-TENANT-BOUNDARY",
            scene_code="reimbursement",
            title="租户边界测试",
            current_stage="claiming",
            status="active",
        )
        link = ExpenseCaseLink(
            id="link-tenant-boundary",
            tenant_id="tenant-a",
            expense_case_id=expense_case.id,
            resource_type="expense_claim",
            resource_id=claim.id,
            relation_type="claim",
        )
        db.add_all([claim, expense_case, link])
        db.flush()

        with pytest.raises(PermissionError, match="tenant does not match"):
            RiskObservationService(db).upsert_observation(
                {
                    **_observation_payload("risk:tenant-boundary"),
                    "claim_id": claim.id,
                },
                tenant_id="tenant-b",
            )

        assert (
            db.query(RiskObservation)
            .filter_by(observation_key="risk:tenant-boundary")
            .one_or_none()
            is None
        )


def test_hermes_global_scan_builds_graphs_inside_each_tenant(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    with _build_session() as db:
        claims = [
            _claim_orm("claim-tenant-a", "BX-TENANT-A"),
            _claim_orm("claim-tenant-b", "BX-TENANT-B"),
        ]
        claims[0].tenant_id = "tenant-a"
        claims[1].tenant_id = "tenant-b"
        cases = [
            ExpenseCase(
                id=f"case-tenant-{suffix}",
                tenant_id=f"tenant-{suffix}",
                case_no=f"CASE-TENANT-{suffix.upper()}",
                scene_code="reimbursement",
                title="租户隔离图扫描",
                current_stage="claiming",
                status="active",
            )
            for suffix in ("a", "b")
        ]
        links = [
            ExpenseCaseLink(
                id=f"link-tenant-{suffix}",
                tenant_id=f"tenant-{suffix}",
                expense_case_id=cases[index].id,
                resource_type="expense_claim",
                resource_id=claims[index].id,
                relation_type="claim",
            )
            for index, suffix in enumerate(("a", "b"))
        ]
        db.add_all([*claims, *cases, *links])
        db.flush()

        evaluated_claim_sets: list[set[str]] = []
        history_tenants: list[str] = []

        def fake_evaluate(context):
            evaluated_claim_sets.append(set(context.target_claim_ids))
            return SimpleNamespace(observations=[], nodes=[], edges=[])

        def fake_history(_self, *, tenant_id=None, **_kwargs):
            history_tenants.append(str(tenant_id))
            return []

        scanner = HermesRiskScannerService(db)
        monkeypatch.setattr(
            scanner,
            "_fetch_unscanned_claims",
            lambda *, tenant_id: [claim for claim in claims if claim.tenant_id == tenant_id],
        )
        monkeypatch.setattr(
            "app.services.hermes_risk_scanner.evaluate_financial_risk_graph",
            fake_evaluate,
        )
        monkeypatch.setattr(RiskObservationService, "build_history_stats", fake_history)

        summary_a = scanner.scan_global_risks(tenant_id="tenant-a")
        summary_b = scanner.scan_global_risks(tenant_id="tenant-b")

        assert evaluated_claim_sets == [
            {"claim-tenant-a"},
            {"claim-tenant-b"},
        ]
        assert history_tenants == ["tenant-a", "tenant-b"]
        assert summary_a["scanned_claim_count"] == 1
        assert summary_b["scanned_claim_count"] == 1


def test_risk_scan_discards_snapshot_after_claim_changes_during_evaluation(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    with _build_session() as db:
        claim = _claim_orm("claim-scan-stale", "BX-SCAN-STALE")
        db.add(claim)
        db.commit()
        original_updated_at = claim.updated_at

        def fake_evaluate(_context):
            claim.status = "pending_payment"
            claim.approval_stage = "待付款"
            claim.updated_at = original_updated_at + timedelta(seconds=1)
            db.flush()
            return SimpleNamespace(observations=[], nodes=[], edges=[])

        scanner = HermesRiskScannerService(db)
        monkeypatch.setattr(
            scanner,
            "_fetch_unscanned_claims",
            lambda *, tenant_id: [claim] if claim.tenant_id == tenant_id else [],
        )
        monkeypatch.setattr(
            "app.services.hermes_risk_scanner.evaluate_financial_risk_graph",
            fake_evaluate,
        )

        summary = scanner.scan_global_risks()

        db.refresh(claim)
        assert summary["scanned_claim_count"] == 0
        assert claim.status == "pending_payment"
        assert claim.hermes_scanned_at is None


def test_risk_observation_feedback_pool_fields_and_replay_set_contract() -> None:
    with _build_session() as db:
        service = RiskObservationService(db)
        service.upsert_observation(_observation_payload("risk:c1:duplicate_invoice"))
        db.commit()

        feedback = service.create_feedback(
            "risk:c1:duplicate_invoice",
            RiskObservationFeedbackCreate(
                feedback_type="comment",
                action="rewrite",
                actor="auditor",
                comment="建议生成候选规则",
                payload_json={
                    "decision": "candidate_rule_rewrite",
                    "candidate_rule_source": "risk_observation_feedback",
                    "confidence_score": 0.76,
                    "escalation_target": "finance_manager",
                    "supplement_required": True,
                },
            ),
        )
        observation = service.get_observation("risk:c1:duplicate_invoice")
        assert observation is not None

        replay_set = AlgorithmReplaySetBuilder().build_from_observations(
            "replay-set-1",
            [
                {
                    "observation_key": observation.observation_key,
                    "claim_id": observation.claim_id,
                    "risk_signal": observation.risk_signal,
                    "risk_score": observation.risk_score,
                    "risk_level": observation.risk_level,
                    "algorithm_version": observation.algorithm_version,
                    "feedback_status": observation.feedback_status,
                    "ontology_json": observation.ontology_json,
                    "decision_trace": observation.decision_trace_json,
                }
            ],
            created_at=datetime(2026, 5, 30, tzinfo=UTC),
        )

        assert feedback.decision == "candidate_rule_rewrite"
        assert feedback.candidate_rule_source == "risk_observation_feedback"
        assert feedback.confidence_score == 0.76
        assert feedback.escalation_target == "finance_manager"
        assert feedback.supplement_required is True
        assert replay_set.replay_set_id == "replay-set-1"
        assert replay_set.cases[0].claim_id == "c1"
        assert replay_set.cases[0].ontology_version == "ontology.v1"
        assert replay_set.cases[0].algorithm_version == "financial_risk_graph.v1"


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


def _build_client() -> tuple[TestClient, sessionmaker[Session]]:
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
    app = FastAPI()
    app.include_router(risk_observations_router, prefix="/api/v1")
    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 _observation_payload(observation_key: str) -> dict:
    return {
        "observation_key": observation_key,
        "subject_type": "expense_claim",
        "subject_key": "claim:c1",
        "subject_label": "BX-001",
        "claim_id": "c1",
        "claim_no": "BX-001",
        "risk_type": "duplicate_invoice",
        "risk_signal": "duplicate_invoice",
        "title": "Duplicate invoice risk",
        "description": "Same invoice appears in multiple claims.",
        "risk_score": 86,
        "risk_level": "high",
        "confidence_score": "0.81",
        "control_stage": "reimbursement",
        "control_mode": "risk_observation",
        "automation_mode": "semi_auto_review",
        "source": "financial_risk_graph",
        "algorithm_version": "financial_risk_graph.v1",
        "contribution_scores": {"S_rule": 82, "S_graph": 95},
        "baseline": {"scope": "expense_type", "sample_size": 4},
        "evidence": [
            {
                "code": "duplicate_invoice_graph",
                "source": "graph",
                "metadata": {"vendor_name": "上海差旅供应商"},
            }
        ],
        "graph_node_keys": ["claim:c1", "vendor:上海差旅供应商"],
        "graph_edge_keys": [],
        "policy_refs": ["policy.duplicate_invoice"],
        "similar_case_claim_ids": ["c2"],
        "ontology_json": {
            "gate": "review",
            "ontology_parse_id": "parse-1",
            "ontology_version": "ontology.v1",
            "domain": "expense",
            "scenario": "reimbursement",
            "intent": "risk_check",
            "ontology_entities_json": [{"type": "claim", "value": "c1"}],
            "risk_signals_json": [{"code": "duplicate_invoice"}],
            "canonical_subject_key": "claim:c1",
        },
        "decision_trace": {
            "formula": "weighted",
            "sampling_strategy": {"strategy": "focused_review", "threshold": 70},
            "evaluation_case_id": "case-duplicate-invoice",
        },
    }


def _employee_orm() -> Employee:
    return Employee(
        id="emp-risk",
        employee_no="E-RISK",
        name="风险员工",
        email="risk.employee@example.com",
        position="高级专员",
        grade="P6",
    )


def _claim_orm(claim_id: str, claim_no: str) -> ExpenseClaim:
    now = datetime(2026, 5, 20, tzinfo=UTC)
    return ExpenseClaim(
        id=claim_id,
        claim_no=claim_no,
        employee_id="emp-risk",
        employee_name="风险员工",
        department_id="dept-risk",
        department_name="风控部",
        expense_type="travel",
        reason="客户拜访",
        location="上海",
        amount=Decimal("1200"),
        currency="CNY",
        invoice_count=1,
        occurred_at=now,
        submitted_at=now,
        status="submitted",
        approval_stage="manager_review",
        risk_flags_json=[],
    )
