from __future__ import annotations

import json
from datetime import UTC, date, datetime
from decimal import Decimal
from typing import Any

from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentAssetType
from app.models.agent_asset import AgentAsset
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
from app.services.expense_claim_attachment_storage import ExpenseClaimAttachmentStorage
from app.services.expense_claims import ExpenseClaimService
from app.services.risk_rule_generation_interpreter import COMPOSITE_RULE_TEMPLATE_KEY
from app.test_helpers.db import build_in_memory_session

build_session = build_in_memory_session


def _build_rule_payload(
    *,
    rule_code: str,
    name: str,
    business_stage: str,
    message: str,
    expense_category: str = "travel",
) -> dict[str, Any]:
    return {
        "schema_version": "2.0",
        "rule_code": rule_code,
        "name": name,
        "evaluator": "template_rule",
        "enabled": True,
        "applies_to": {
            "domains": [AgentAssetDomain.EXPENSE.value],
            "business_stages": [business_stage],
            "expense_categories": [expense_category],
        },
        "template_key": COMPOSITE_RULE_TEMPLATE_KEY,
        "params": {
            "template_key": COMPOSITE_RULE_TEMPLATE_KEY,
            "field_keys": ["claim.reason"],
            "conditions": [
                {
                    "id": "missing_exception_reason",
                    "operator": "not_contains_any",
                    "fields": ["claim.reason"],
                    "keywords": ["专项审批"],
                }
            ],
            "hit_logic": {"all": ["missing_exception_reason"]},
            "message_template": message,
        },
        "outcomes": {"fail": {"severity": "high", "action": "manual_review"}},
    }


def _add_active_rule_asset(
    db: Session,
    manager: AgentAssetRuleLibraryManager,
    *,
    rule_code: str,
    business_stage: str,
    message: str,
    expense_category: str = "travel",
) -> None:
    file_name = f"{rule_code}.json"
    payload = _build_rule_payload(
        rule_code=rule_code,
        name=message,
        business_stage=business_stage,
        message=message,
        expense_category=expense_category,
    )
    manager.write_rule_library_json(
        library=RISK_RULES_LIBRARY,
        file_name=file_name,
        payload=payload,
    )
    db.add(
        AgentAsset(
            asset_type=AgentAssetType.RULE.value,
            code=rule_code,
            name=message,
            description="",
            domain=AgentAssetDomain.EXPENSE.value,
            scenario_json=["差旅费"],
            owner="pytest",
            status=AgentAssetStatus.ACTIVE.value,
            current_version="v1.0.0",
            published_version="v1.0.0",
            config_json={
                "detail_mode": "json_risk",
                "rule_library": RISK_RULES_LIBRARY,
                "rule_document": {"file_name": file_name},
            },
        )
    )


def _add_vague_goods_rule_asset(
    db: Session,
    manager: AgentAssetRuleLibraryManager,
) -> None:
    rule_code = "risk.travel.low.vague_ticket_content"
    file_name = f"{rule_code}.json"
    payload = {
        "schema_version": "2.0",
        "rule_code": rule_code,
        "name": "差旅票据服务内容笼统低风险",
        "description": "票据商品或服务名称过于笼统，提醒补充明细。",
        "evaluator": "vague_goods_description",
        "enabled": True,
        "requires_attachment": True,
        "applies_to": {
            "domains": ["expense", "travel"],
            "expense_types": ["travel"],
            "business_stages": ["reimbursement"],
        },
        "outcomes": {"fail": {"severity": "low", "action": "warning"}},
    }
    manager.write_rule_library_json(
        library=RISK_RULES_LIBRARY,
        file_name=file_name,
        payload=payload,
    )
    db.add(
        AgentAsset(
            asset_type=AgentAssetType.RULE.value,
            code=rule_code,
            name="差旅票据服务内容笼统低风险",
            description="",
            domain=AgentAssetDomain.EXPENSE.value,
            scenario_json=["差旅费"],
            owner="pytest",
            status=AgentAssetStatus.ACTIVE.value,
            current_version="v1.0.0",
            published_version="v1.0.0",
            config_json={
                "detail_mode": "json_risk",
                "rule_library": RISK_RULES_LIBRARY,
                "rule_document": {"file_name": file_name},
            },
        )
    )


def _add_multi_city_reason_rule_asset(
    db: Session,
    manager: AgentAssetRuleLibraryManager,
) -> None:
    rule_code = "risk.travel.medium.multi_city_no_reason"
    file_name = f"{rule_code}.json"
    payload = {
        "schema_version": "2.0",
        "rule_code": rule_code,
        "name": "多城市行程缺少说明中风险",
        "evaluator": "multi_city_reason_required",
        "enabled": True,
        "requires_attachment": True,
        "applies_to": {
            "domains": ["expense", "travel"],
            "expense_types": ["travel"],
            "business_stages": ["reimbursement"],
        },
        "outcomes": {"fail": {"severity": "medium", "action": "manual_review"}},
    }
    manager.write_rule_library_json(
        library=RISK_RULES_LIBRARY,
        file_name=file_name,
        payload=payload,
    )
    db.add(
        AgentAsset(
            asset_type=AgentAssetType.RULE.value,
            code=rule_code,
            name="多城市行程缺少说明中风险",
            description="",
            domain=AgentAssetDomain.EXPENSE.value,
            scenario_json=["差旅费"],
            owner="pytest",
            status=AgentAssetStatus.ACTIVE.value,
            current_version="v1.0.0",
            published_version="v1.0.0",
            config_json={
                "detail_mode": "json_risk",
                "rule_library": RISK_RULES_LIBRARY,
                "rule_document": {"file_name": file_name},
            },
        )
    )


def _write_attachment_meta(storage_root, invoice_id: str, meta: dict[str, Any]) -> None:
    file_path = storage_root / invoice_id
    file_path.parent.mkdir(parents=True, exist_ok=True)
    file_path.write_bytes(b"attachment")
    file_path.with_name(f"{file_path.name}.meta.json").write_text(
        f"{json.dumps(meta, ensure_ascii=False, indent=2)}\n",
        encoding="utf-8",
    )


def _build_claim(*, claim_no: str, expense_type: str, status: str = "draft") -> ExpenseClaim:
    return ExpenseClaim(
        tenant_id="default",
        claim_no=claim_no,
        employee_name="张三",
        department_name="研发部",
        project_code=None,
        expense_type=expense_type,
        reason="去上海处理客户现场问题",
        location="上海",
        amount=Decimal("1200.00"),
        currency="CNY",
        invoice_count=0,
        occurred_at=datetime(2026, 5, 30, tzinfo=UTC),
        submitted_at=None,
        status=status,
        approval_stage="待提交",
        risk_flags_json=[],
    )


def _patch_rule_manager(monkeypatch, manager: AgentAssetRuleLibraryManager) -> None:
    from app.services import expense_claim_platform_risk

    monkeypatch.setattr(
        expense_claim_platform_risk,
        "AgentAssetRuleLibraryManager",
        lambda: manager,
    )


def test_platform_risk_rules_are_filtered_by_business_stage_and_category(
    tmp_path,
    monkeypatch,
) -> None:
    with build_session() as db:
        manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
        _patch_rule_manager(monkeypatch, manager)
        _add_active_rule_asset(
            db,
            manager,
            rule_code="application.stage.rule",
            business_stage="expense_application",
            message="申请环节规则命中",
        )
        _add_active_rule_asset(
            db,
            manager,
            rule_code="reimbursement.stage.rule",
            business_stage="reimbursement",
            message="报账环节规则命中",
        )
        _add_active_rule_asset(
            db,
            manager,
            rule_code="risk.budget.sample.reimbursement.rule",
            business_stage="reimbursement",
            message="预算风险规则不应进入行为风险检测",
        )
        _add_active_rule_asset(
            db,
            manager,
            rule_code="office.application.rule",
            business_stage="expense_application",
            message="办公申请规则不应命中差旅",
            expense_category="office",
        )
        db.commit()

        service = ExpenseClaimService(db)
        application_claim = _build_claim(
            claim_no="APP-TEST-001",
            expense_type="travel_application",
        )
        reimbursement_claim = _build_claim(
            claim_no="CLM-TEST-001",
            expense_type="travel",
        )
        db.add_all([application_claim, reimbursement_claim])
        db.commit()

        application_review = service.evaluate_platform_risk_rules(
            application_claim,
            business_stage="expense_application",
        )
        reimbursement_review = service.evaluate_platform_risk_rules(
            reimbursement_claim,
            business_stage="reimbursement",
        )

        assert [flag["rule_code"] for flag in application_review["flags"]] == [
            "application.stage.rule"
        ]
        assert application_review["flags"][0]["message"] == "申请环节规则命中"
        assert application_review["flags"][0]["business_stage"] == "expense_application"
        # 申请单阶段信息完整性（policy 域）风险对申请人可见，便于申请时知晓并修正
        assert application_review["flags"][0]["visibility_scope"] == "submitter"
        assert application_review["flags"][0]["actionability"] == "fixable_by_submitter"
        assert [flag["rule_code"] for flag in reimbursement_review["flags"]] == [
            "reimbursement.stage.rule"
        ]
        assert reimbursement_review["flags"][0]["message"] == "报账环节规则命中"
        assert reimbursement_review["flags"][0]["business_stage"] == "reimbursement"
        assert reimbursement_review["flags"][0]["visibility_scope"] == "submitter"
        assert reimbursement_review["flags"][0]["actionability"] == "fixable_by_submitter"


def test_expense_application_pre_review_runs_stage_rules(tmp_path, monkeypatch) -> None:
    with build_session() as db:
        manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
        _patch_rule_manager(monkeypatch, manager)
        _add_active_rule_asset(
            db,
            manager,
            rule_code="application.pre.review.rule",
            business_stage="expense_application",
            message="申请预审规则命中",
        )
        claim = _build_claim(claim_no="APP-TEST-002", expense_type="travel_application")
        db.add(claim)
        db.commit()

        current_user = CurrentUserContext(
            tenant_id="default",
            username="张三",
            name="张三",
            role_codes=[],
            is_admin=False,
            department_name="研发部",
        )
        reviewed = ExpenseClaimService(db).pre_review_claim(claim.id, current_user)

        assert reviewed is not None
        rule_flags = [
            flag
            for flag in reviewed.risk_flags_json
            if isinstance(flag, dict) and flag.get("rule_code") == "application.pre.review.rule"
        ]
        assert len(rule_flags) == 1
        assert rule_flags[0]["message"] == "申请预审规则命中"
        assert rule_flags[0]["business_stage"] == "expense_application"
        assert rule_flags[0]["risk_domain"] == "policy"
        # 申请单阶段信息完整性风险对申请人可见
        assert rule_flags[0]["visibility_scope"] == "submitter"
        ai_pre_review = [
            flag
            for flag in reviewed.risk_flags_json
            if isinstance(flag, dict) and flag.get("source") == "ai_pre_review"
        ][0]
        assert ai_pre_review["passed"] is False
        assert ai_pre_review["blocking_risk_count"] == 1
        assert ai_pre_review["business_stage"] == "expense_application"


def test_preapproval_amount_rules_run_from_rule_library() -> None:
    with build_session() as db:
        claim = _build_claim(claim_no="RE-PREAPPROVAL-MEAL", expense_type="meal")
        claim.amount = Decimal("501.00")
        db.add(claim)
        db.commit()

        flags = ExpenseClaimService(db).evaluate_platform_risk_rules(
            claim,
            business_stage="reimbursement",
        )["flags"]

        meal_flags = [
            flag
            for flag in flags
            if isinstance(flag, dict)
            and flag.get("rule_code") == "risk.application.meal_high_value_without_preapproval"
        ]
        assert len(meal_flags) == 1
        assert meal_flags[0]["finance_rule_code"] == "expense.preapproval.policy"
        assert "500" in meal_flags[0]["message"]

        claim.risk_flags_json = [
            {
                "source": "application_link",
                "application_claim_id": "application-preapproval-ok",
                "application_claim_no": "AP-202606-OK",
            }
        ]
        flags = ExpenseClaimService(db).evaluate_platform_risk_rules(
            claim,
            business_stage="reimbursement",
        )["flags"]
        assert all(
            flag.get("rule_code") != "risk.application.meal_high_value_without_preapproval"
            for flag in flags
            if isinstance(flag, dict)
        )


def test_reimbursement_item_sync_persists_rule_center_risk_preview(
    tmp_path,
    monkeypatch,
) -> None:
    with build_session() as db:
        manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
        _patch_rule_manager(monkeypatch, manager)
        _add_active_rule_asset(
            db,
            manager,
            rule_code="reimbursement.preview.rule",
            business_stage="reimbursement",
            message="报销风险预判命中",
        )
        claim = _build_claim(claim_no="RE-TEST-001", expense_type="travel")
        claim.invoice_count = 1
        claim.items = [
            ExpenseClaimItem(
                item_date=date(2026, 5, 30),
                item_type="travel",
                item_reason="客户现场支持",
                item_location="上海",
                item_amount=Decimal("1200.00"),
                invoice_id="ticket.pdf",
            )
        ]
        db.add(claim)
        db.commit()

        service = ExpenseClaimService(db)
        service._sync_claim_from_items(claim)

        rule_flags = [
            flag
            for flag in claim.risk_flags_json
            if isinstance(flag, dict) and flag.get("rule_code") == "reimbursement.preview.rule"
        ]
        assert len(rule_flags) == 1
        assert rule_flags[0]["message"] == "报销风险预判命中"
        assert rule_flags[0]["severity"] == "high"
        assert rule_flags[0]["business_stage"] == "reimbursement"
        assert rule_flags[0]["visibility_scope"] == "submitter"
        assert rule_flags[0]["actionability"] == "fixable_by_submitter"


def test_vague_ticket_content_ignores_clear_hotel_receipt_text(
    tmp_path,
    monkeypatch,
) -> None:
    with build_session() as db:
        manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
        storage_root = tmp_path / "attachments"
        _patch_rule_manager(monkeypatch, manager)
        monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: storage_root)
        _add_vague_goods_rule_asset(db, manager)

        invoice_id = "claim-clear-hotel/item-hotel/hotel.jpg"
        claim = _build_claim(claim_no="RE-CLEAR-HOTEL-001", expense_type="travel")
        claim.invoice_count = 1
        claim.items = [
            ExpenseClaimItem(
                item_date=date(2026, 2, 23),
                item_type="hotel_ticket",
                item_reason="上海喜来登酒店",
                item_location="上海",
                item_amount=Decimal("828.00"),
                invoice_id=invoice_id,
            )
        ]
        db.add(claim)
        db.commit()
        _write_attachment_meta(
            storage_root,
            invoice_id,
            {
                "document_info": {
                    "document_type": "hotel_invoice",
                    "document_type_label": "酒店住宿票据",
                    "scene_code": "hotel",
                    "scene_label": "住宿票据",
                    "fields": [
                        {"key": "amount", "label": "金额", "value": "828元"},
                        {"key": "date", "label": "日期", "value": "2026-02-23"},
                        {"key": "merchant_name", "label": "商户", "value": "上海喜来登酒店"},
                    ],
                },
                "ocr_summary": "上海喜来登酒店；住宿发票",
                "ocr_text": "本发票仅含住宿费，不含其他增值服务费。",
            },
        )

        review = ExpenseClaimService(db).evaluate_platform_risk_rules(
            claim,
            business_stage="reimbursement",
        )

        assert not [
            flag
            for flag in review["flags"]
            if isinstance(flag, dict)
            and flag.get("rule_code") == "risk.travel.low.vague_ticket_content"
        ]


def test_vague_ticket_content_still_flags_unclear_goods_name(
    tmp_path,
    monkeypatch,
) -> None:
    with build_session() as db:
        manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
        storage_root = tmp_path / "attachments"
        _patch_rule_manager(monkeypatch, manager)
        monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: storage_root)
        _add_vague_goods_rule_asset(db, manager)

        invoice_id = "claim-vague/item-other/other.pdf"
        claim = _build_claim(claim_no="RE-VAGUE-001", expense_type="travel")
        claim.invoice_count = 1
        claim.items = [
            ExpenseClaimItem(
                item_date=date(2026, 2, 23),
                item_type="other",
                item_reason="差旅相关补充票据",
                item_location="上海",
                item_amount=Decimal("200.00"),
                invoice_id=invoice_id,
            )
        ]
        db.add(claim)
        db.commit()
        _write_attachment_meta(
            storage_root,
            invoice_id,
            {
                "document_info": {
                    "document_type": "other",
                    "document_type_label": "其他单据",
                    "scene_code": "other",
                    "scene_label": "其他票据",
                    "fields": [
                        {"key": "goods_name", "label": "商品或服务名称", "value": "服务费"},
                    ],
                },
                "ocr_summary": "费用发票",
                "ocr_text": "项目：服务费。",
            },
        )

        review = ExpenseClaimService(db).evaluate_platform_risk_rules(
            claim,
            business_stage="reimbursement",
        )
        rule_flags = [
            flag
            for flag in review["flags"]
            if isinstance(flag, dict)
            and flag.get("rule_code") == "risk.travel.low.vague_ticket_content"
        ]

        assert len(rule_flags) == 1
        assert rule_flags[0]["severity"] == "low"
        assert rule_flags[0]["evidence"]["matched_keywords"] == ["服务费"]


def test_multi_city_reason_risk_marks_only_abnormal_route_items(
    tmp_path,
    monkeypatch,
) -> None:
    with build_session() as db:
        manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
        storage_root = tmp_path / "attachments"
        _patch_rule_manager(monkeypatch, manager)
        monkeypatch.setattr(ExpenseClaimAttachmentStorage, "root", lambda self: storage_root)
        _add_multi_city_reason_rule_asset(db, manager)

        claim = _build_claim(claim_no="RE-MULTI-CITY-001", expense_type="travel")
        claim.location = "上海"
        claim.reason = "支撑国网仿生产环境部署"
        routes = [
            ("outbound", "武汉-上海", date(2026, 2, 20), Decimal("354.00")),
            ("extra-out", "上海-深圳", date(2026, 2, 21), Decimal("438.00")),
            ("extra-back", "深圳-上海", date(2026, 2, 22), Decimal("388.00")),
            ("return", "上海-武汉", date(2026, 2, 23), Decimal("354.00")),
        ]
        claim.items = [
            ExpenseClaimItem(
                item_date=item_date,
                item_type="train_ticket",
                item_reason=route,
                item_location=route,
                item_amount=amount,
                invoice_id=f"claim-multi-city/{suffix}.pdf",
            )
            for suffix, route, item_date, amount in routes
        ]
        db.add(claim)
        db.commit()

        for suffix, route, _, _ in routes:
            _write_attachment_meta(
                storage_root,
                f"claim-multi-city/{suffix}.pdf",
                {
                    "document_info": {
                        "document_type": "train_ticket",
                        "document_type_label": "火车票",
                        "scene_code": "travel",
                        "scene_label": "交通票据",
                        "fields": [
                            {"key": "route", "label": "路线", "value": route},
                        ],
                    },
                    "ocr_summary": f"火车票；{route}",
                    "ocr_text": f"旅客行程为 {route}",
                },
            )

        review = ExpenseClaimService(db).evaluate_platform_risk_rules(
            claim,
            business_stage="reimbursement",
        )
        rule_flags = [
            flag
            for flag in review["flags"]
            if isinstance(flag, dict)
            and flag.get("rule_code") == "risk.travel.medium.multi_city_no_reason"
        ]

        assert len(rule_flags) == 1
        flagged_item_ids = set(rule_flags[0]["item_ids"])
        route_item_ids = {item.item_reason: item.id for item in claim.items}
        assert flagged_item_ids == {
            route_item_ids["上海-深圳"],
            route_item_ids["深圳-上海"],
        }
        assert route_item_ids["武汉-上海"] not in flagged_item_ids
        assert route_item_ids["上海-武汉"] not in flagged_item_ids
