from __future__ import annotations

from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime
from decimal import Decimal

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

from app.api.deps import CurrentUserContext
from app.db.base import Base
from app.models.approval_action import ApprovalActionLedger
from app.models.audit_log import AuditLog
from app.models.employee import Employee
from app.models.expense_case import BusinessEvent
from app.models.financial_record import ExpenseClaim
from app.schemas.reimbursement import ExpenseClaimRead
from app.services.approval_action_protocol import (
    ApprovalActionConflictError,
    ApprovalActionProtocol,
)
from app.services.expense_claims import ExpenseClaimService


def _manager_user() -> CurrentUserContext:
    return CurrentUserContext(
        tenant_id="default",
        username="manager-action@example.com",
        name="李经理",
        role_codes=["manager"],
        is_admin=False,
    )


def _finance_user() -> CurrentUserContext:
    return CurrentUserContext(
        tenant_id="default",
        username="finance-action@example.com",
        name="王财务",
        role_codes=["finance"],
        is_admin=False,
    )


def _seed_claim(
    db: Session,
    *,
    claim_id: str = "claim-action-1",
    manager_email: str = "manager-action@example.com",
) -> ExpenseClaim:
    manager = Employee(
        id=f"manager-{claim_id}",
        employee_no=f"M-{claim_id}",
        name="李经理",
        email=manager_email,
    )
    employee = Employee(
        id=f"employee-{claim_id}",
        employee_no=f"E-{claim_id}",
        name="张三",
        email=f"employee-{claim_id}@example.com",
        manager=manager,
    )
    claim = ExpenseClaim(
        id=claim_id,
        claim_no=f"EXP-{claim_id}",
        employee=employee,
        employee_name="张三",
        department_name="市场部",
        expense_type="transport",
        reason="客户拜访",
        location="上海",
        amount=Decimal("88.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=[],
    )
    db.add(claim)
    db.commit()
    return claim


@pytest.fixture
def session_factory() -> sessionmaker[Session]:
    engine = create_engine("sqlite+pysqlite:///:memory:")
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
    try:
        yield factory
    finally:
        engine.dispose()


def test_approve_replay_persists_one_ledger_event_and_audit(
    session_factory: sessionmaker[Session],
) -> None:
    with session_factory() as db:
        claim = _seed_claim(db)
        service = ExpenseClaimService(db)
        first = service.approve_claim(
            claim.id,
            _manager_user(),
            opinion="同意",
            request_id="approve-retry-1",
            expected_status="submitted",
            expected_approval_stage="直属领导审批",
        )
        replay = service.approve_claim(
            claim.id,
            _manager_user(),
            opinion="同意",
            request_id="approve-retry-1",
            expected_status="submitted",
            expected_approval_stage="直属领导审批",
        )

        assert first is not None and replay is not None
        assert ExpenseClaimRead.model_validate(replay).model_dump(mode="json") == (
            ExpenseClaimRead.model_validate(first).model_dump(mode="json")
        )
        assert replay.approval_stage == "财务审批"
        assert db.scalar(select(func.count()).select_from(ApprovalActionLedger)) == 1
        assert db.scalar(select(func.count()).select_from(BusinessEvent)) == 1
        assert (
            db.scalar(
                select(func.count())
                .select_from(AuditLog)
                .where(AuditLog.request_id == "approve-retry-1")
            )
            == 1
        )


def test_approve_replay_returns_original_snapshot_after_claim_moves_forward(
    session_factory: sessionmaker[Session],
) -> None:
    with session_factory() as db:
        claim = _seed_claim(db, claim_id="claim-action-original-response")
        service = ExpenseClaimService(db)
        first = service.approve_claim(
            claim.id,
            _manager_user(),
            opinion="同意",
            request_id="approve-original-response-1",
            expected_status="submitted",
            expected_approval_stage="直属领导审批",
        )
        assert first is not None
        first_json = ExpenseClaimRead.model_validate(first).model_dump(mode="json")

        current = db.get(ExpenseClaim, claim.id)
        assert current is not None
        current.status = "pending_payment"
        current.approval_stage = "待付款"
        current.risk_flags_json = [
            *list(current.risk_flags_json or []),
            {"source": "future_state", "message": "首次响应之后产生的内部状态"},
        ]
        db.commit()

        replay = service.approve_claim(
            claim.id,
            _manager_user(),
            opinion="同意",
            request_id="approve-original-response-1",
            expected_status="submitted",
            expected_approval_stage="直属领导审批",
        )

        assert replay is not None
        assert ExpenseClaimRead.model_validate(replay).model_dump(mode="json") == first_json
        assert replay.approval_stage == "财务审批"
        assert all(item.get("source") != "future_state" for item in replay.risk_flags_json or [])
        persisted = db.get(ExpenseClaim, claim.id)
        assert persisted is not None
        assert persisted.status == "pending_payment"
        assert persisted.approval_stage == "待付款"
        ledger = db.scalar(
            select(ApprovalActionLedger).where(
                ApprovalActionLedger.request_id == "approve-original-response-1"
            )
        )
        assert ledger is not None
        assert ledger.response_json == first_json


def test_request_id_payload_mismatch_and_stale_preconditions_return_conflict(
    session_factory: sessionmaker[Session],
) -> None:
    with session_factory() as db:
        claim = _seed_claim(db, claim_id="claim-action-conflict")
        service = ExpenseClaimService(db)
        with pytest.raises(ApprovalActionConflictError, match="单据状态已从"):
            service.approve_claim(
                claim.id,
                _manager_user(),
                opinion="同意",
                request_id="approve-stale-1",
                expected_status="draft",
                expected_approval_stage="直属领导审批",
            )
        service.approve_claim(
            claim.id,
            _manager_user(),
            opinion="同意",
            request_id="approve-conflict-1",
            expected_status="submitted",
            expected_approval_stage="直属领导审批",
        )

        with pytest.raises(ApprovalActionConflictError, match="已用于另一项"):
            service.approve_claim(
                claim.id,
                _manager_user(),
                opinion="改为有条件通过",
                request_id="approve-conflict-1",
                expected_status="submitted",
                expected_approval_stage="直属领导审批",
            )

        assert (
            db.scalar(
                select(func.count())
                .select_from(ApprovalActionLedger)
                .where(ApprovalActionLedger.request_id == "approve-stale-1")
            )
            == 0
        )


def test_action_failure_rolls_back_ledger_claim_event_and_audit(
    session_factory: sessionmaker[Session],
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    with session_factory() as db:
        claim = _seed_claim(db, claim_id="claim-action-rollback")
        service = ExpenseClaimService(db)

        def fail_completion(*args, **kwargs):
            raise RuntimeError("ledger completion failed")

        monkeypatch.setattr(ApprovalActionProtocol, "complete", fail_completion)
        with pytest.raises(RuntimeError, match="ledger completion failed"):
            service.approve_claim(
                claim.id,
                _manager_user(),
                opinion="同意",
                request_id="approve-rollback-1",
                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
        assert db.scalar(select(func.count()).select_from(BusinessEvent)) == 0
        assert db.scalar(select(func.count()).select_from(AuditLog)) == 0


def test_legacy_stage_repair_cannot_commit_inside_action_protocol(
    session_factory: sessionmaker[Session],
) -> None:
    with session_factory() as db:
        claim = _seed_claim(db, claim_id="claim-action-stage-repair")
        claim.approval_stage = "预算管理者审批"
        claim.risk_flags_json = [
            {
                "source": "manual_approval",
                "event_type": "expense_claim_approval",
                "previous_approval_stage": "直属领导审批",
                "next_approval_stage": "预算管理者审批",
                "operator": "李经理",
                "next_approver_name": "李经理",
            }
        ]
        db.commit()
        admin_user = CurrentUserContext(
            tenant_id="default",
            username="admin-action@example.com",
            name="审批管理员",
            role_codes=["admin"],
            is_admin=True,
        )

        with pytest.raises(ApprovalActionConflictError, match="审批节点已从"):
            ExpenseClaimService(db).approve_claim(
                claim.id,
                admin_user,
                opinion="同意",
                request_id="approve-stage-repair-1",
                expected_status="submitted",
                expected_approval_stage="预算管理者审批",
            )

        db.expire_all()
        persisted = db.get(ExpenseClaim, claim.id)
        assert persisted is not None
        assert persisted.approval_stage == "预算管理者审批"
        assert db.scalar(select(func.count()).select_from(ApprovalActionLedger)) == 0


def test_return_and_pay_actions_use_the_same_protocol(
    session_factory: sessionmaker[Session],
) -> None:
    with session_factory() as db:
        returned_claim = _seed_claim(db, claim_id="claim-action-return")
        paid_claim = _seed_claim(
            db,
            claim_id="claim-action-pay",
            manager_email="manager-pay-action@example.com",
        )
        paid_claim.status = "pending_payment"
        paid_claim.approval_stage = "待付款"
        db.commit()

        returned = ExpenseClaimService(db).return_claim(
            returned_claim.id,
            _manager_user(),
            reason="请补充材料",
            request_id="return-action-1",
            expected_status="submitted",
            expected_approval_stage="直属领导审批",
        )
        paid = ExpenseClaimService(db).mark_claim_paid(
            paid_claim.id,
            _finance_user(),
            request_id="pay-action-1",
            expected_status="pending_payment",
            expected_approval_stage="待付款",
        )

        assert returned is not None and returned.status == "returned"
        assert paid is not None and paid.status == "paid"
        ledgers = list(
            db.scalars(select(ApprovalActionLedger).order_by(ApprovalActionLedger.action)).all()
        )
        assert [(item.action, item.result_status) for item in ledgers] == [
            ("pay", "paid"),
            ("return", "returned"),
        ]


def test_concurrent_identical_request_executes_once(tmp_path) -> None:
    engine = create_engine(
        f"sqlite+pysqlite:///{tmp_path / 'approval-action.db'}",
        connect_args={"check_same_thread": False},
    )
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
    with factory() as db:
        _seed_claim(db, claim_id="claim-action-concurrent")

    def approve() -> str:
        with factory() as db:
            result = ExpenseClaimService(db).approve_claim(
                "claim-action-concurrent",
                _manager_user(),
                opinion="同意",
                request_id="approve-concurrent-1",
                expected_status="submitted",
                expected_approval_stage="直属领导审批",
            )
            assert result is not None
            return str(result.approval_stage)

    try:
        with ThreadPoolExecutor(max_workers=2) as pool:
            results = list(pool.map(lambda _: approve(), range(2)))
        assert results == ["财务审批", "财务审批"]
        with factory() as db:
            assert db.scalar(select(func.count()).select_from(ApprovalActionLedger)) == 1
            assert db.scalar(select(func.count()).select_from(BusinessEvent)) == 1
    finally:
        engine.dispose()
