from __future__ import annotations

import os
import threading
import time
import uuid
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.engine import make_url
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.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.schemas.risk_disposition import RiskDispositionActionCreate
from app.services.expense_claim_risk_gate import ExpenseClaimRiskBlockedError
from app.services.expense_claims import ExpenseClaimService
from app.services.risk_dispositions import RiskDispositionService

DATABASE_URL = os.environ.get("MIGRATION_TEST_DATABASE_URL", "").strip()


def test_disposition_reopen_and_approval_share_claim_lock(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    database_url = _require_disposable_database_url()
    monkeypatch.setenv("FEW_SHOT_INJECTION_ENABLED", "false")
    engine = create_engine(database_url, pool_pre_ping=True)
    Base.metadata.create_all(bind=engine)
    factory = sessionmaker(bind=engine, expire_on_commit=False)
    suffix = uuid.uuid4().hex[:12]
    claim_id = f"claim-risk-lock-{suffix}"
    observation_id = f"risk-lock-{suffix}"
    disposition_id = f"disposition-lock-{suffix}"
    manager_email = f"manager-{suffix}@example.com"
    manager_user = CurrentUserContext(
        tenant_id="default",
        username=manager_email,
        name="并发审批经理",
        role_codes=["manager"],
        is_admin=False,
    )
    with factory() as db:
        _seed_locked_risk_case(
            db,
            claim_id=claim_id,
            observation_id=observation_id,
            disposition_id=disposition_id,
            manager_email=manager_email,
            suffix=suffix,
        )

    claim_locked = threading.Event()
    release_disposition = threading.Event()
    approval_started = threading.Event()
    from app.services import risk_dispositions as risk_disposition_module

    original_apply_action = risk_disposition_module._apply_action

    def pause_after_claim_lock(*args, **kwargs):
        claim_locked.set()
        if not release_disposition.wait(timeout=5):
            raise TimeoutError("test did not release risk disposition")
        return original_apply_action(*args, **kwargs)

    monkeypatch.setattr(risk_disposition_module, "_apply_action", pause_after_claim_lock)

    def reopen_risk() -> str:
        with factory() as db:
            result = RiskDispositionService(db).execute_action(
                observation_id,
                RiskDispositionActionCreate(
                    action="confirm",
                    expected_version=1,
                    request_id=f"request-risk-reopen-{suffix}",
                    comment="复核后确认风险成立",
                ),
                tenant_id="default",
                actor_id=manager_email,
                actor_name="并发审批经理",
                current_user=manager_user,
            )
            return result.disposition.adjudication

    def approve_claim() -> str:
        approval_started.set()
        with factory() as db:
            try:
                ExpenseClaimService(db).approve_claim(
                    claim_id,
                    manager_user,
                    opinion="同意",
                    request_id=f"request-approve-after-risk-{suffix}",
                    expected_status="submitted",
                    expected_approval_stage="直属领导审批",
                )
            except ExpenseClaimRiskBlockedError:
                return "blocked"
        return "approved"

    try:
        with ThreadPoolExecutor(max_workers=2) as pool:
            risk_future = pool.submit(reopen_risk)
            assert claim_locked.wait(timeout=5)
            approval_future = pool.submit(approve_claim)
            assert approval_started.wait(timeout=5)
            time.sleep(0.2)
            assert not approval_future.done()
            release_disposition.set()
            assert risk_future.result(timeout=5) == "confirmed"
            assert approval_future.result(timeout=5) == "blocked"

        with factory() as db:
            claim = db.get(ExpenseClaim, claim_id)
            disposition = db.get(RiskDisposition, disposition_id)
            assert claim is not None and claim.approval_stage == "直属领导审批"
            assert disposition is not None and disposition.adjudication == "confirmed"
            assert (
                db.scalar(
                    select(func.count())
                    .select_from(ApprovalActionLedger)
                    .where(ApprovalActionLedger.claim_id == claim_id)
                )
                == 0
            )
    finally:
        release_disposition.set()
        engine.dispose()


def _seed_locked_risk_case(
    db: Session,
    *,
    claim_id: str,
    observation_id: str,
    disposition_id: str,
    manager_email: str,
    suffix: str,
) -> None:
    manager = Employee(
        id=f"manager-risk-lock-{suffix}",
        tenant_id="default",
        employee_no=f"M-RISK-LOCK-{suffix}",
        name="并发审批经理",
        email=manager_email,
    )
    employee = Employee(
        id=f"employee-risk-lock-{suffix}",
        tenant_id="default",
        employee_no=f"E-RISK-LOCK-{suffix}",
        name="并发风险员工",
        email=f"employee-{suffix}@example.com",
        manager=manager,
    )
    now = datetime.now(UTC)
    claim = ExpenseClaim(
        id=claim_id,
        tenant_id="default",
        claim_no=f"EXP-RISK-LOCK-{suffix}",
        employee=employee,
        employee_name=employee.name,
        department_name="风控部",
        expense_type="travel",
        reason="客户拜访",
        location="上海",
        amount=Decimal("1200.00"),
        currency="CNY",
        invoice_count=1,
        occurred_at=now,
        submitted_at=now,
        status="submitted",
        approval_stage="直属领导审批",
        risk_flags_json=[],
    )
    observation = RiskObservation(
        id=observation_id,
        tenant_id="default",
        observation_key=f"risk:claim-lock:{suffix}",
        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=92,
        risk_level="high",
        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="false_positive",
        feedback_status="false_positive",
    )
    disposition = RiskDisposition(
        id=disposition_id,
        tenant_id="default",
        observation_id=observation_id,
        adjudication="false_positive",
        lifecycle_status="open",
        version=1,
    )
    db.add_all([manager, employee, claim, observation, disposition])
    db.commit()


def _require_disposable_database_url() -> str:
    if not DATABASE_URL:
        pytest.skip("仅在显式配置 MIGRATION_TEST_DATABASE_URL 时运行 PostgreSQL 并发测试")
    parsed = make_url(DATABASE_URL)
    host = str(parsed.host or "").replace("_", "-").lower()
    database = str(parsed.database or "").replace("_", "-").lower()
    if not host.startswith(("migration-probe", "disposable-probe")):
        raise RuntimeError("并发测试数据库主机必须使用 disposable 前缀")
    if not database.startswith(("migration-probe", "disposable-probe")):
        raise RuntimeError("并发测试数据库名必须使用 disposable 前缀")
    return DATABASE_URL
