from __future__ import annotations

import uuid
from datetime import UTC, datetime
from decimal import Decimal

import pytest
from sqlalchemy import create_engine, or_
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

from app.api.deps import CurrentUserContext
from app.db.base import Base
from app.models.budget import BudgetAllocation
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.models.organization import OrganizationUnit
from app.models.role import Role
from app.services.expense_claim_errors import ExpenseClaimPreReviewBlockedError
from app.services.expense_claim_workflow_constants import (
    APPLICATION_LINK_STATUS_STAGE,
    BUDGET_MANAGER_APPROVAL_STAGE,
    DIRECT_MANAGER_APPROVAL_STAGE,
    FINANCE_APPROVAL_STAGE,
)
from app.services.expense_claims import ExpenseClaimService


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 reimbursement_claim_query(db: Session):
    return db.query(ExpenseClaim).filter(
        or_(
            ExpenseClaim.claim_no.like("RE-%"),
            ExpenseClaim.claim_no.like("R________"),
        )
    )


def _seed_budget_monitor_role(db: Session) -> Role:
    role = Role(role_code="budget_monitor", name="预算管理员")
    db.add(role)
    db.flush()
    return role


def _seed_budget_allocation(
    db: Session,
    *,
    department_id: str,
    department_name: str,
    amount: Decimal = Decimal("50000.00"),
) -> None:
    db.add(
        BudgetAllocation(
            budget_no=f"BUD-ROUTE-{uuid.uuid4().hex[:8]}",
            fiscal_year=2026,
            period_type="quarter",
            period_key="2026Q2",
            department_id=department_id,
            department_name=department_name,
            cost_center=None,
            project_code=None,
            subject_code="travel",
            subject_name="差旅",
            original_amount=amount,
            adjusted_amount=Decimal("0.00"),
            status="active",
            warning_threshold=Decimal("80.00"),
            control_action="block",
        )
    )
    db.flush()


def _seed_people(
    db: Session, *, suffix: str
) -> tuple[OrganizationUnit, Employee, Employee, Employee]:
    budget_role = _seed_budget_monitor_role(db)
    department = OrganizationUnit(
        unit_code=f"ROUTE-{suffix}",
        name=f"动态路由部{suffix}",
        unit_type="department",
    )
    manager = Employee(
        employee_no=f"M-{suffix}",
        name=f"直属领导{suffix}",
        email=f"manager-{suffix}@example.com",
        organization_unit=department,
    )
    budget_manager = Employee(
        employee_no=f"B-{suffix}",
        name=f"预算管理员{suffix}",
        email=f"budget-{suffix}@example.com",
        grade="P8",
        organization_unit=department,
        roles=[budget_role],
    )
    employee = Employee(
        employee_no=f"E-{suffix}",
        name=f"申请人{suffix}",
        email=f"employee-{suffix}@example.com",
        manager=manager,
        organization_unit=department,
    )
    db.add_all([department, manager, budget_manager, employee])
    db.flush()
    return department, manager, budget_manager, employee


def test_low_risk_application_skips_budget_manager_and_generates_draft() -> None:
    with build_session() as db:
        department, manager, _budget_manager, employee = _seed_people(db, suffix="LOW-APP")
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
        )
        claim = ExpenseClaim(
            claim_no="APP-20260530-LOW-ROUTE",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            project_code=None,
            expense_type="travel_application",
            reason="客户现场沟通",
            location="上海",
            amount=Decimal("500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
            risk_flags_json=[],
        )
        db.add(claim)
        db.commit()

        approved = ExpenseClaimService(db).approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=manager.email,
                name=manager.name,
                role_codes=["manager"],
                is_admin=False,
            ),
            opinion="业务必要，同意申请",
        )

        assert approved is not None
        assert approved.status == "approved"
        assert approved.approval_stage == APPLICATION_LINK_STATUS_STAGE
        assert reimbursement_claim_query(db).count() == 1
        assert any(
            isinstance(flag, dict)
            and flag.get("source") == "approval_routing"
            and flag.get("requires_budget_review") is False
            and flag.get("route") == "approval_done"
            and flag.get("business_stage") == "expense_application"
            for flag in approved.risk_flags_json
        )
        assert any(
            isinstance(flag, dict)
            and flag.get("source") == "manual_approval"
            and flag.get("next_approval_stage") == APPLICATION_LINK_STATUS_STAGE
            and flag.get("route_decision", {}).get("requires_budget_review") is False
            for flag in approved.risk_flags_json
        )


def test_budget_warning_application_still_skips_budget_manager_when_not_over_budget() -> None:
    with build_session() as db:
        department, manager, _budget_manager, employee = _seed_people(db, suffix="WARN-APP")
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
            amount=Decimal("10000.00"),
        )
        claim = ExpenseClaim(
            claim_no="APP-20260530-WARN-ROUTE",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            project_code=None,
            expense_type="travel_application",
            reason="客户现场支持",
            location="上海",
            amount=Decimal("8500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
            risk_flags_json=[
                {
                    "source": "budget_control",
                    "event_type": "budget_warning",
                    "severity": "medium",
                    "label": "预算接近预警线",
                    "message": "预算仍可承接，但审批后使用率将接近预警线。",
                }
            ],
        )
        db.add(claim)
        db.commit()

        approved = ExpenseClaimService(db).approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=manager.email,
                name=manager.name,
                role_codes=["manager"],
                is_admin=False,
            ),
            opinion="业务必要，同意申请。",
        )

        assert approved is not None
        assert approved.status == "approved"
        assert approved.approval_stage == APPLICATION_LINK_STATUS_STAGE
        assert any(
            isinstance(flag, dict)
            and flag.get("source") == "approval_routing"
            and flag.get("requires_budget_review") is False
            and flag.get("route") == "approval_done"
            for flag in approved.risk_flags_json
        )
        assert not any(
            isinstance(flag, dict)
            and flag.get("source") == "manual_approval"
            and flag.get("next_approval_stage") == BUDGET_MANAGER_APPROVAL_STAGE
            for flag in approved.risk_flags_json
        )


def test_application_routes_to_budget_manager_when_usage_reaches_90_percent() -> None:
    with build_session() as db:
        department, manager, budget_manager, employee = _seed_people(db, suffix="OVER-90-APP")
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
            amount=Decimal("10000.00"),
        )
        claim = ExpenseClaim(
            claim_no="APP-20260530-OVER-90",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            project_code=None,
            expense_type="travel_application",
            reason="客户现场支持",
            location="上海",
            amount=Decimal("9500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
            risk_flags_json=[],
        )
        db.add(claim)
        db.commit()

        routed = ExpenseClaimService(db).approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=manager.email,
                name=manager.name,
                role_codes=["manager"],
                is_admin=False,
            ),
            opinion="业务必要，同意申请。",
        )

        assert routed is not None
        assert routed.status == "submitted"
        assert routed.approval_stage == BUDGET_MANAGER_APPROVAL_STAGE
        assert any(
            isinstance(flag, dict)
            and flag.get("source") == "approval_routing"
            and flag.get("requires_budget_review") is True
            and flag.get("route") == "budget_manager"
            and any("90%" in item or "90" in item for item in flag.get("reasons", []))
            for flag in routed.risk_flags_json
        )

        with pytest.raises(ValueError, match="预算已超过警戒值"):
            ExpenseClaimService(db).approve_claim(
                claim.id,
                CurrentUserContext(
                    tenant_id="default",
                    username=budget_manager.email,
                    name=budget_manager.name,
                    role_codes=["budget_monitor"],
                    is_admin=False,
                ),
                opinion="   ",
            )


def test_high_risk_application_under_90_percent_routes_to_budget_manager() -> None:
    with build_session() as db:
        department, manager, _budget_manager, employee = _seed_people(db, suffix="RISK-APP")
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
            amount=Decimal("10000.00"),
        )
        claim = ExpenseClaim(
            claim_no="APP-20260530-RISK-UNDER-90",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            project_code=None,
            expense_type="travel_application",
            reason="客户现场支持",
            location="上海",
            amount=Decimal("500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
            risk_flags_json=[
                {
                    "source": "submission_review",
                    "severity": "high",
                    "actionability": "route_review",
                    "label": "申请信息风险",
                    "message": "申请事由需要领导关注。",
                    "business_stage": "expense_application",
                }
            ],
        )
        db.add(claim)
        db.commit()

        approved = ExpenseClaimService(db).approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=manager.email,
                name=manager.name,
                role_codes=["manager"],
                is_admin=False,
            ),
            opinion="业务必要，同意申请。",
        )

        assert approved is not None
        assert approved.status == "submitted"
        assert approved.approval_stage == BUDGET_MANAGER_APPROVAL_STAGE
        route_flag = [
            flag
            for flag in approved.risk_flags_json
            if isinstance(flag, dict) and flag.get("source") == "approval_routing"
        ][0]
        assert route_flag["requires_budget_review"] is True
        assert route_flag["route"] == "budget_manager"
        assert route_flag["current_risk_count"] == 1
        assert any("申请信息风险" in reason for reason in route_flag["reasons"])


def test_fixable_high_risk_application_is_blocked_before_direct_manager() -> None:
    with build_session() as db:
        department, _manager, _budget_manager, employee = _seed_people(
            db,
            suffix="FIXABLE-RISK-APP",
        )
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
            amount=Decimal("10000.00"),
        )
        claim = ExpenseClaim(
            claim_no="APP-20260530-FIXABLE-RISK",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            expense_type="travel_application",
            reason="客户现场支持",
            location="上海",
            amount=Decimal("500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            status="draft",
            approval_stage="待提交",
            risk_flags_json=[
                {
                    "source": "manual_risk",
                    "severity": "high",
                    "actionability": "fixable_by_submitter",
                    "label": "申请事由不完整",
                    "message": "请先补充客户与项目说明。",
                    "business_stage": "expense_application",
                }
            ],
        )
        db.add(claim)
        db.commit()
        current_user = CurrentUserContext(
            username=employee.email,
            name=employee.name,
            employee_id=employee.id,
            role_codes=["user"],
            is_admin=False,
            tenant_id="default",
        )
        service = ExpenseClaimService(db)

        reviewed = service.pre_review_claim(claim.id, current_user)
        assert reviewed is not None
        pre_review_flag = next(
            flag
            for flag in reviewed.risk_flags_json
            if isinstance(flag, dict) and flag.get("source") == "ai_pre_review"
        )
        assert pre_review_flag["decision"] == "needs_fix"

        with pytest.raises(ExpenseClaimPreReviewBlockedError):
            service.submit_claim(
                claim.id,
                current_user,
                pre_review_id=pre_review_flag["review_id"],
                pre_review_input_fingerprint=pre_review_flag["input_fingerprint"],
            )

        assert db.get(ExpenseClaim, claim.id).status == "draft"


def test_review_decision_high_risk_application_flows_from_pre_review_to_p8() -> None:
    with build_session() as db:
        department, manager, _budget_manager, employee = _seed_people(
            db,
            suffix="REVIEW-RISK-APP",
        )
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
            amount=Decimal("10000.00"),
        )
        claim = ExpenseClaim(
            claim_no="APP-20260530-REVIEW-RISK",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            expense_type="travel_application",
            reason="客户现场支持",
            location="上海",
            amount=Decimal("500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            status="draft",
            approval_stage="待提交",
            risk_flags_json=[
                {
                    "source": "manual_risk",
                    "severity": "high",
                    "actionability": "route_review",
                    "label": "特殊项目风险",
                    "message": "该项目需预算管理者确认。",
                    "business_stage": "expense_application",
                }
            ],
        )
        db.add(claim)
        db.commit()
        current_user = CurrentUserContext(
            username=employee.email,
            name=employee.name,
            employee_id=employee.id,
            role_codes=["user"],
            is_admin=False,
            tenant_id="default",
        )
        service = ExpenseClaimService(db)

        reviewed = service.pre_review_claim(claim.id, current_user)
        assert reviewed is not None
        pre_review_flag = next(
            flag
            for flag in reviewed.risk_flags_json
            if isinstance(flag, dict) and flag.get("source") == "ai_pre_review"
        )
        assert pre_review_flag["decision"] == "ready_with_review"
        submitted = service.submit_claim(
            claim.id,
            current_user,
            pre_review_id=pre_review_flag["review_id"],
            pre_review_input_fingerprint=pre_review_flag["input_fingerprint"],
        )
        assert submitted is not None
        assert submitted.approval_stage == DIRECT_MANAGER_APPROVAL_STAGE

        routed = service.approve_claim(
            claim.id,
            CurrentUserContext(
                username=manager.email,
                name=manager.name,
                role_codes=["manager"],
                is_admin=False,
                tenant_id="default",
            ),
            opinion="业务必要，同意申请。",
        )
        assert routed is not None
        assert routed.approval_stage == BUDGET_MANAGER_APPROVAL_STAGE


def test_application_route_ignores_reimbursement_stage_current_risks() -> None:
    with build_session() as db:
        department, manager, _budget_manager, employee = _seed_people(db, suffix="MIXED-STAGE")
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
        )
        claim = ExpenseClaim(
            claim_no="APP-20260530-MIXED-STAGE",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            project_code=None,
            expense_type="travel_application",
            reason="客户现场沟通",
            location="上海",
            amount=Decimal("500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
            risk_flags_json=[
                {
                    "source": "submission_review",
                    "severity": "high",
                    "label": "报销票据风险",
                    "message": "报销票据城市与行程城市不一致。",
                    "business_stage": "reimbursement",
                }
            ],
        )
        db.add(claim)
        db.commit()

        approved = ExpenseClaimService(db).approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=manager.email,
                name=manager.name,
                role_codes=["manager"],
                is_admin=False,
            ),
            opinion="业务必要，同意申请。",
        )

        assert approved is not None
        assert approved.status == "approved"
        assert approved.approval_stage == APPLICATION_LINK_STATUS_STAGE
        route_flag = [
            flag
            for flag in approved.risk_flags_json
            if isinstance(flag, dict) and flag.get("source") == "approval_routing"
        ][0]
        assert route_flag["requires_budget_review"] is False
        assert route_flag["current_risk_count"] == 0
        assert route_flag["business_stage"] == "expense_application"


def test_risky_reimbursement_routes_to_budget_then_finance() -> None:
    with build_session() as db:
        department, manager, budget_manager, employee = _seed_people(db, suffix="RISK-CLAIM")
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
        )
        claim = ExpenseClaim(
            claim_no="RE-20260530-RISK-ROUTE",
            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("600.00"),
            currency="CNY",
            invoice_count=1,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
            risk_flags_json=[
                {
                    "source": "submission_review",
                    "severity": "high",
                    "actionability": "route_review",
                    "label": "行程城市异常",
                    "message": "票据城市与申报目的地不一致",
                }
            ],
        )
        db.add(claim)
        db.commit()
        service = ExpenseClaimService(db)

        routed = service.approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=manager.email,
                name=manager.name,
                role_codes=["manager"],
                is_admin=False,
            ),
            opinion="业务属实，同意报账",
        )

        assert routed is not None
        assert routed.status == "submitted"
        assert routed.approval_stage == BUDGET_MANAGER_APPROVAL_STAGE
        assert any(
            isinstance(flag, dict)
            and flag.get("source") == "approval_routing"
            and flag.get("requires_budget_review") is True
            and flag.get("route") == "budget_manager"
            and any("行程城市异常" in item for item in flag.get("reasons", []))
            for flag in routed.risk_flags_json
        )

        budget_approved = service.approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=budget_manager.email,
                name=budget_manager.name,
                role_codes=["budget_monitor"],
                is_admin=False,
            ),
            opinion="预算影响已复核，同意进入财务审批",
        )

        assert budget_approved is not None
        assert budget_approved.status == "submitted"
        assert budget_approved.approval_stage == FINANCE_APPROVAL_STAGE
        assert any(
            isinstance(flag, dict)
            and flag.get("source") == "budget_approval"
            and flag.get("event_type") == "expense_claim_budget_approval"
            and flag.get("next_approval_stage") == FINANCE_APPROVAL_STAGE
            for flag in budget_approved.risk_flags_json
        )


def test_resolved_high_risk_application_does_not_route_to_budget_manager() -> None:
    with build_session() as db:
        department, manager, _budget_manager, employee = _seed_people(
            db,
            suffix="RESOLVED-RISK-APP",
        )
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
            amount=Decimal("10000.00"),
        )
        claim = ExpenseClaim(
            claim_no="APP-20260530-RESOLVED-RISK",
            employee_id=employee.id,
            employee_name=employee.name,
            department_id=department.id,
            department_name=department.name,
            project_code=None,
            expense_type="travel_application",
            reason="客户现场支持",
            location="上海",
            amount=Decimal("500.00"),
            currency="CNY",
            invoice_count=0,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage=DIRECT_MANAGER_APPROVAL_STAGE,
            risk_flags_json=[
                {
                    "source": "submission_review",
                    "severity": "high",
                    "label": "已整改风险",
                    "message": "申请人已完成补充说明。",
                    "business_stage": "expense_application",
                    "resolution_status": "resolved",
                }
            ],
        )
        db.add(claim)
        db.commit()

        routed = ExpenseClaimService(db).approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=manager.email,
                name=manager.name,
                role_codes=["manager"],
                is_admin=False,
            ),
            opinion="风险已整改，同意申请。",
        )

        assert routed is not None
        assert routed.status == "approved"
        assert routed.approval_stage == APPLICATION_LINK_STATUS_STAGE


def test_budget_manager_blank_opinion_defaults_to_agree_when_budget_under_warning() -> None:
    with build_session() as db:
        department, _manager, budget_manager, employee = _seed_people(db, suffix="BUDGET-NORMAL")
        _seed_budget_allocation(
            db,
            department_id=department.id,
            department_name=department.name,
            amount=Decimal("10000.00"),
        )
        claim = ExpenseClaim(
            claim_no="RE-20260530-BUDGET-NORMAL",
            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("500.00"),
            currency="CNY",
            invoice_count=1,
            occurred_at=datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
            submitted_at=datetime(2026, 5, 30, 10, 0, tzinfo=UTC),
            status="submitted",
            approval_stage=BUDGET_MANAGER_APPROVAL_STAGE,
            risk_flags_json=[],
        )
        db.add(claim)
        db.commit()

        approved = ExpenseClaimService(db).approve_claim(
            claim.id,
            CurrentUserContext(
                tenant_id="default",
                username=budget_manager.email,
                name=budget_manager.name,
                role_codes=["budget_monitor"],
                is_admin=False,
            ),
            opinion="   ",
        )

        assert approved is not None
        assert approved.status == "submitted"
        assert approved.approval_stage == FINANCE_APPROVAL_STAGE
        assert any(
            isinstance(flag, dict)
            and flag.get("source") == "budget_approval"
            and flag.get("event_type") == "expense_claim_budget_approval"
            and flag.get("opinion") == "同意"
            for flag in approved.risk_flags_json
        )
