from __future__ import annotations

from datetime import UTC, datetime, timedelta
from decimal import Decimal

import pytest
from sqlalchemy import create_engine, func, select
from sqlalchemy.orm import Session

from app.api.deps import CurrentUserContext
from app.db.base import Base
from app.models.approval_action import ApprovalActionLedger
from app.models.employee import Employee
from app.models.financial_record import ExpenseClaim
from app.models.risk_disposition import RiskDisposition
from app.models.risk_observation import RiskObservation
from app.services.expense_claim_risk_gate import (
    ExpenseClaimRiskBlockedError,
    ExpenseClaimRiskGate,
)
from app.services.expense_claims import ExpenseClaimService


def test_high_risk_requires_false_positive_or_resolved_disposition() -> None:
    with _session() as db:
        claim = _claim()
        observation = _observation(claim)
        db.add_all([claim, observation])
        db.commit()
        gate = ExpenseClaimRiskGate(db)

        with pytest.raises(ExpenseClaimRiskBlockedError):
            gate.ensure_approvable(claim, tenant_id="default")

        disposition = RiskDisposition(
            tenant_id="default",
            observation_id=observation.id,
            adjudication="false_positive",
            lifecycle_status="open",
        )
        db.add(disposition)
        db.commit()
        gate.ensure_approvable(claim, tenant_id="default")

        disposition.adjudication = "confirmed"
        db.commit()
        with pytest.raises(ExpenseClaimRiskBlockedError):
            gate.ensure_approvable(claim, tenant_id="default")

        disposition.lifecycle_status = "resolved"
        db.commit()
        gate.ensure_approvable(claim, tenant_id="default")


def test_only_unexpired_approved_waiver_bypasses_risk_gate() -> None:
    with _session() as db:
        claim = _claim(claim_id="claim-risk-waiver-gate")
        observation = _observation(claim, observation_id="risk-waiver-gate")
        now = datetime.now(UTC)
        disposition = RiskDisposition(
            tenant_id="default",
            observation_id=observation.id,
            adjudication="confirmed",
            lifecycle_status="waived",
            waiver_requester_id="waiver-requester",
            waiver_requester_name="豁免申请人",
            waiver_requested_at=now - timedelta(days=2),
            waiver_reason="临时无法取得纸质材料。",
            waiver_scope="仅限本次客户拜访。",
            waiver_expires_at=now + timedelta(days=2),
            waiver_conditions_json=["到期前补件"],
            waiver_decision="approved",
            waiver_decider_id="waiver-decider",
            waiver_decider_name="豁免决定人",
            waiver_decided_at=now - timedelta(days=1),
            waiver_decision_reason="补偿控制充分，同意限时豁免。",
        )
        db.add_all([claim, observation, disposition])
        db.commit()
        gate = ExpenseClaimRiskGate(db)

        gate.ensure_approvable(claim, tenant_id="default")

        disposition.waiver_expires_at = now - timedelta(hours=1)
        db.commit()
        with pytest.raises(ExpenseClaimRiskBlockedError):
            gate.ensure_approvable(claim, tenant_id="default")

        disposition.lifecycle_status = "waiver_rejected"
        disposition.waiver_decision = "rejected"
        disposition.waiver_decision_reason = "补偿控制不足，拒绝豁免。"
        db.commit()
        with pytest.raises(ExpenseClaimRiskBlockedError):
            gate.ensure_approvable(claim, tenant_id="default")


def test_medium_and_foreign_tenant_risks_do_not_block_claim() -> None:
    with _session() as db:
        claim = _claim(claim_id="claim-risk-nonblocking")
        medium = _observation(claim, observation_id="risk-medium", risk_level="medium")
        foreign = _observation(
            claim,
            observation_id="risk-foreign",
            risk_level="critical",
            tenant_id="tenant-b",
        )
        db.add_all([claim, medium, foreign])
        db.commit()

        ExpenseClaimRiskGate(db).ensure_approvable(claim, tenant_id="default")


def test_unmaterialized_raw_high_risk_blocks_approval() -> None:
    with _session() as db:
        claim = _claim(claim_id="claim-risk-raw-only")
        claim.risk_flags_json = [
            {
                "source": "attachment_analysis",
                "severity": "high",
                "label": "票据金额异常",
                "message": "票据金额与申报金额不一致。",
                "triggered": True,
            }
        ]
        db.add(claim)
        db.commit()

        with pytest.raises(ExpenseClaimRiskBlockedError) as captured:
            ExpenseClaimRiskGate(db).ensure_approvable(claim, tenant_id="default")

        assert captured.value.blockers[0].observation_id.startswith("raw:")
        assert captured.value.blockers[0].risk_level == "high"


def test_persisted_observation_does_not_hide_another_raw_high_risk() -> None:
    with _session() as db:
        claim = _claim(claim_id="claim-risk-partial-materialization")
        claim.risk_flags_json = [
            {
                "source": "attachment_analysis",
                "severity": "critical",
                "label": "另一条未物化风险",
                "triggered": True,
            }
        ]
        low_observation = _observation(
            claim,
            observation_id="risk-low-materialized",
            risk_level="low",
        )
        db.add_all([claim, low_observation])
        db.commit()

        with pytest.raises(ExpenseClaimRiskBlockedError) as captured:
            ExpenseClaimRiskGate(db).ensure_approvable(claim, tenant_id="default")

        assert [item.risk_level for item in captured.value.blockers] == ["critical"]


def test_blocked_approval_rolls_back_action_ledger_and_claim_mutation() -> None:
    with _session() as db:
        manager = Employee(
            id="manager-risk-gate",
            employee_no="M-RISK-GATE",
            name="风险经理",
            email="risk-gate-manager@example.com",
        )
        employee = Employee(
            id="employee-risk-gate",
            employee_no="E-RISK-GATE",
            name="风险员工",
            email="risk-gate-employee@example.com",
            manager=manager,
        )
        claim = _claim(employee=employee, claim_id="claim-risk-blocked-approval")
        db.add_all([manager, employee, claim, _observation(claim)])
        db.commit()

        with pytest.raises(ExpenseClaimRiskBlockedError):
            ExpenseClaimService(db).approve_claim(
                claim.id,
                CurrentUserContext(
                    tenant_id="default",
                    username=manager.email,
                    name=manager.name,
                    role_codes=["manager"],
                    is_admin=False,
                ),
                opinion="同意",
                request_id="risk-blocked-approval-001",
                expected_status="submitted",
                expected_approval_stage="直属领导审批",
            )

        db.expire_all()
        persisted = db.get(ExpenseClaim, claim.id)
        assert persisted is not None
        assert persisted.status == "submitted"
        assert persisted.approval_stage == "直属领导审批"
        assert db.scalar(select(func.count()).select_from(ApprovalActionLedger)) == 0


def _session() -> Session:
    engine = create_engine("sqlite+pysqlite:///:memory:")
    Base.metadata.create_all(bind=engine)
    return Session(engine)


def _claim(
    *,
    claim_id: str = "claim-risk-gate",
    employee: Employee | None = None,
) -> ExpenseClaim:
    now = datetime(2026, 7, 16, tzinfo=UTC)
    return ExpenseClaim(
        id=claim_id,
        claim_no=f"EXP-{claim_id}",
        employee=employee,
        employee_name=employee.name if employee else "风险员工",
        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="直属领导审批",
        risk_flags_json=[],
    )


def _observation(
    claim: ExpenseClaim,
    *,
    observation_id: str = "risk-gate-observation",
    risk_level: str = "high",
    tenant_id: str = "default",
) -> RiskObservation:
    return RiskObservation(
        id=f"{observation_id}-{claim.id}",
        tenant_id=tenant_id,
        observation_key=f"risk:{tenant_id}:{observation_id}:{claim.id}",
        subject_type="expense_claim",
        subject_key=f"claim:{claim.id}",
        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=90,
        risk_level=risk_level,
        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",
    )
