from __future__ import annotations

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 sqlalchemy.pool import StaticPool

from app.db.base import Base
from app.models.ai_learning import AIDecision, AIDecisionFeedback, WorkflowOutcome
from app.models.expense_case import BusinessEvent
from app.models.financial_record import ExpenseClaim
from app.models.risk_observation import RiskObservation
from app.schemas.risk_disposition import RiskDispositionActionCreate
from app.services.expense_cases import ExpenseCaseService
from app.services.expense_workflow_learning import ExpenseWorkflowLearningService
from app.services.risk_dispositions import RiskDispositionService


def test_workflow_events_create_idempotent_verified_learning_evidence() -> None:
    with _build_session() as db:
        claim, decision, pre_review = _seed_submitted_decision(db, tenant_id="tenant-a")
        service = ExpenseWorkflowLearningService(db)

        service.record_prior_events_for_decision(decision)
        service.record_prior_events_for_decision(decision)

        audit_outcome = db.scalar(
            select(WorkflowOutcome).where(WorkflowOutcome.business_event_id == pre_review.id)
        )
        audit_feedback = db.scalar(
            select(AIDecisionFeedback).where(
                AIDecisionFeedback.correlation_id == pre_review.correlation_id,
                AIDecisionFeedback.action_type == "audit_cleared",
            )
        )
        assert audit_outcome is not None
        assert audit_outcome.outcome_type == "audit_cleared"
        assert audit_outcome.outcome_status == "verified"
        assert audit_feedback is not None
        assert audit_feedback.feedback_type == "accepted"
        assert audit_feedback.verification_status == "server_verified"
        assert audit_feedback.training_eligible is True
        assert decision.training_eligible is True
        assert "客户端伪造审计备注" not in str(audit_outcome.result_json)
        assert "客户端伪造审计备注" not in str(audit_feedback.final_value_json)

        claim.status = "returned"
        claim.approval_stage = "待提交"
        _, returned = ExpenseCaseService(db).record_claim_event(
            claim,
            event_type="application_returned",
            actor_id="manager-a",
            tenant_id="tenant-a",
            correlation_id="return-flow-1",
            idempotency_key="return-flow-1",
            extra_payload={"reason": "不要把自由文本变成高置信记忆"},
        )
        _, replayed = ExpenseCaseService(db).record_claim_event(
            claim,
            event_type="application_returned",
            actor_id="manager-a",
            tenant_id="tenant-a",
            correlation_id="return-flow-1",
            idempotency_key="return-flow-1",
            extra_payload={"reason": "重放时也不得进入学习证据"},
        )
        assert replayed.id == returned.id

        return_outcome = db.scalar(
            select(WorkflowOutcome).where(WorkflowOutcome.business_event_id == returned.id)
        )
        return_feedback = db.scalar(
            select(AIDecisionFeedback).where(AIDecisionFeedback.action_type == "workflow_returned")
        )
        assert return_outcome is not None
        assert return_outcome.outcome_type == "workflow_returned"
        assert "自由文本" not in str(return_outcome.result_json)
        assert return_feedback is not None
        assert return_feedback.feedback_type == "rejected"
        assert return_feedback.verification_status == "human_verified"
        assert return_feedback.changed_fields_json == []
        assert db.scalar(select(func.count()).select_from(WorkflowOutcome)) == 2
        assert db.scalar(select(func.count()).select_from(AIDecisionFeedback)) == 2


def test_approval_pass_override_and_payment_are_separate_outcomes() -> None:
    with _build_session() as db:
        claim, decision, _ = _seed_submitted_decision(db, tenant_id="tenant-a")
        case_service = ExpenseCaseService(db)

        claim.approval_stage = "财务审批"
        _, passed = case_service.record_claim_event(
            claim,
            event_type="approval_stage_completed",
            actor_id="manager-a",
            tenant_id="tenant-a",
            correlation_id="approval-pass-1",
            idempotency_key="approval-pass-1",
            extra_payload={"opinion": "自由审批意见不进入反馈"},
        )
        _, overridden = case_service.record_claim_event(
            claim,
            event_type="approval_stage_completed",
            actor_id="finance-a",
            tenant_id="tenant-a",
            correlation_id="approval-override-1",
            idempotency_key="approval-override-1",
            extra_payload={
                "ai_decision_overridden": True,
                "opinion": "覆盖原因也不进入高置信证据",
            },
        )
        claim.status = "paid"
        claim.approval_stage = "已付款"
        _, paid = case_service.record_claim_event(
            claim,
            event_type="payment_completed",
            actor_id="finance-a",
            tenant_id="tenant-a",
            correlation_id="payment-1",
            idempotency_key="payment-1",
            extra_payload={"payment_note": "付款备注不得被学习"},
        )

        outcomes = list(
            db.scalars(
                select(WorkflowOutcome).where(
                    WorkflowOutcome.business_event_id.in_([passed.id, overridden.id, paid.id])
                )
            ).all()
        )
        assert {item.outcome_type for item in outcomes} == {
            "approval_passed",
            "approval_overridden",
            "payment_completed",
        }
        assert all(item.tenant_id == "tenant-a" for item in outcomes)
        assert all(item.decision_id == decision.id for item in outcomes)
        assert "审批意见" not in str([item.result_json for item in outcomes])
        assert "付款备注" not in str([item.result_json for item in outcomes])

        feedback = list(
            db.scalars(
                select(AIDecisionFeedback).where(
                    AIDecisionFeedback.correlation_id.in_(
                        ["approval-pass-1", "approval-override-1", "payment-1"]
                    )
                )
            ).all()
        )
        assert {(item.action_type, item.feedback_type) for item in feedback} == {
            ("approval_passed", "accepted"),
            ("approval_overridden", "rejected"),
        }


def test_trusted_outcome_does_not_promote_unverified_client_decision_for_training() -> None:
    with _build_session() as db:
        _, decision, pre_review = _seed_submitted_decision(
            db,
            tenant_id="tenant-client-observed",
        )
        decision.evidence_json = {"trust_level": "behavioral_analytics_only"}
        db.flush()

        ExpenseWorkflowLearningService(db).record_prior_events_for_decision(decision)

        outcome = db.scalar(
            select(WorkflowOutcome).where(WorkflowOutcome.business_event_id == pre_review.id)
        )
        feedback = db.scalar(
            select(AIDecisionFeedback).where(
                AIDecisionFeedback.correlation_id == pre_review.correlation_id
            )
        )
        assert outcome is not None
        assert outcome.outcome_status == "verified"
        assert feedback is not None
        assert feedback.verification_status == "server_verified"
        assert feedback.training_eligible is False
        assert decision.training_eligible is False


def test_later_workflow_event_only_updates_latest_submitted_ai_decision() -> None:
    with _build_session() as db:
        claim, first_decision, _ = _seed_submitted_decision(db, tenant_id="tenant-latest")
        case_service = ExpenseCaseService(db)
        _, second_submission = case_service.record_claim_event(
            claim,
            event_type="application_submitted",
            actor_id="owner-a",
            tenant_id="tenant-latest",
            correlation_id="submit-flow-2",
            idempotency_key="application-submit-2",
        )
        second_decision = AIDecision(
            id="decision-tenant-latest-2",
            tenant_id="tenant-latest",
            expense_case_id=first_decision.expense_case_id,
            business_event_id=second_submission.id,
            expense_claim_id=claim.id,
            correlation_id=second_submission.correlation_id,
            subject_type="expense_claim",
            subject_id=claim.id,
            decision_type="expense_application_prefill",
            decision_source="hybrid",
            status="executed",
            automation_mode="prefill",
            confidence=Decimal("0.9500"),
            suggestion_json={"value_fingerprint": "hmac-sha256:second"},
            evidence_json={"trust_level": "server_snapshot_verified"},
            version_json={"schema_version": 1},
            schema_version=1,
            training_eligible=False,
            idempotency_key="decision-tenant-latest-2-key",
            content_fingerprint="sha256:" + "2" * 64,
            created_at=datetime.now(UTC),
        )
        db.add(second_decision)
        db.flush()

        claim.status = "returned"
        _, returned = case_service.record_claim_event(
            claim,
            event_type="application_returned",
            actor_id="manager-a",
            tenant_id="tenant-latest",
            correlation_id="return-latest",
            idempotency_key="return-latest",
        )

        outcomes = list(
            db.scalars(
                select(WorkflowOutcome).where(WorkflowOutcome.business_event_id == returned.id)
            ).all()
        )
        assert [item.decision_id for item in outcomes] == [second_decision.id]


def test_learning_bridge_rejects_forged_cross_tenant_event() -> None:
    with _build_session() as db:
        claim, _, _ = _seed_submitted_decision(db, tenant_id="tenant-a")
        claim.status = "returned"
        _, trusted = ExpenseCaseService(db).record_claim_event(
            claim,
            event_type="application_returned",
            actor_id="manager-a",
            tenant_id="tenant-a",
            correlation_id="trusted-return",
            idempotency_key="trusted-return",
        )
        forged = BusinessEvent(
            id=trusted.id,
            tenant_id="tenant-b",
            expense_case_id=trusted.expense_case_id,
            aggregate_type=trusted.aggregate_type,
            aggregate_id=trusted.aggregate_id,
            event_type=trusted.event_type,
            event_version=trusted.event_version,
            idempotency_key=trusted.idempotency_key,
            correlation_id=trusted.correlation_id,
            actor_id=trusted.actor_id,
            actor_type=trusted.actor_type,
            payload_json=trusted.payload_json,
            delivery_status="pending",
            occurred_at=trusted.occurred_at,
        )

        with pytest.raises(PermissionError, match="不存在或不属于"):
            ExpenseWorkflowLearningService(db).record_event(forged)

        assert all(
            outcome.tenant_id == "tenant-a" for outcome in db.scalars(select(WorkflowOutcome)).all()
        )


def test_typed_risk_disposition_emits_audit_outcome_without_comment() -> None:
    with _build_session() as db:
        claim, decision, _ = _seed_submitted_decision(db, tenant_id="tenant-a")
        observation = RiskObservation(
            id="risk-observation-1",
            tenant_id="tenant-a",
            observation_key="risk:tenant-a:claim-1",
            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=88,
            risk_level="high",
            confidence_score=0.91,
            control_stage="reimbursement",
            control_mode="risk_observation",
            automation_mode="semi_auto_review",
            source="test",
            algorithm_version="risk.v1",
            status="pending_review",
            feedback_status="unreviewed",
        )
        db.add(observation)
        db.commit()

        service = RiskDispositionService(db)
        payload = RiskDispositionActionCreate(
            action="confirm",
            expected_version=0,
            request_id="risk-confirm-1",
            comment="该人工评论不得进入 AI 学习账本",
        )
        first = service.execute_action(
            observation.id,
            payload,
            tenant_id="tenant-a",
            actor_id="finance-a",
            actor_name="财务甲",
        )
        replay = service.execute_action(
            observation.id,
            payload,
            tenant_id="tenant-a",
            actor_id="finance-a",
            actor_name="财务甲",
        )
        assert replay.event.id == first.event.id

        audit_events = list(
            db.scalars(
                select(BusinessEvent).where(
                    BusinessEvent.tenant_id == "tenant-a",
                    BusinessEvent.event_type == "audit_conclusion_recorded",
                )
            ).all()
        )
        assert len(audit_events) == 1
        assert audit_events[0].payload_json["audit_decision"] == "confirm"
        assert "人工评论" not in str(audit_events[0].payload_json)
        outcome = db.scalar(
            select(WorkflowOutcome).where(WorkflowOutcome.business_event_id == audit_events[0].id)
        )
        assert outcome is not None
        assert outcome.decision_id == decision.id
        assert outcome.outcome_type == "audit_confirmed"
        assert "人工评论" not in str(outcome.result_json)
        assert (
            db.scalar(
                select(func.count())
                .select_from(WorkflowOutcome)
                .where(WorkflowOutcome.business_event_id == audit_events[0].id)
            )
            == 1
        )


def _seed_submitted_decision(
    db: Session,
    *,
    tenant_id: str,
) -> tuple[ExpenseClaim, AIDecision, BusinessEvent]:
    now = datetime(2026, 7, 16, 9, 0, tzinfo=UTC)
    claim = ExpenseClaim(
        id=f"claim-{tenant_id}",
        tenant_id=tenant_id,
        claim_no=f"AP-{tenant_id.upper()}-001",
        employee_name="测试员工",
        department_name="研发部",
        expense_type="travel_application",
        reason="客户拜访",
        location="上海",
        amount=Decimal("1200.00"),
        currency="CNY",
        invoice_count=0,
        occurred_at=now,
        status="draft",
        approval_stage="待提交",
        risk_flags_json=[],
    )
    db.add(claim)
    db.flush()
    case_service = ExpenseCaseService(db)
    _, pre_review = case_service.record_claim_event(
        claim,
        event_type="application_pre_review_completed",
        actor_id="owner-a",
        tenant_id=tenant_id,
        correlation_id="submit-flow-1",
        idempotency_key="pre-review-1",
        update_case_state=False,
        extra_payload={
            "decision": "ready",
            "comment": "客户端伪造审计备注",
        },
    )
    claim.status = "submitted"
    claim.approval_stage = "直属领导审批"
    claim.submitted_at = now
    expense_case, submitted = case_service.record_claim_event(
        claim,
        event_type="application_submitted",
        actor_id="owner-a",
        tenant_id=tenant_id,
        correlation_id="submit-flow-1",
        idempotency_key="application-submit-1",
    )
    decision = AIDecision(
        id=f"decision-{tenant_id}",
        tenant_id=tenant_id,
        expense_case_id=expense_case.id,
        business_event_id=submitted.id,
        expense_claim_id=claim.id,
        correlation_id=submitted.correlation_id,
        subject_type="expense_claim",
        subject_id=claim.id,
        decision_type="expense_application_prefill",
        decision_source="hybrid",
        status="executed",
        automation_mode="prefill",
        confidence=Decimal("0.9000"),
        suggestion_json={"value_fingerprint": "hmac-sha256:test"},
        evidence_json={"trust_level": "server_snapshot_verified"},
        version_json={"schema_version": 1},
        schema_version=1,
        training_eligible=False,
        idempotency_key=f"decision-{tenant_id}-idempotency",
        content_fingerprint="sha256:" + "1" * 64,
        created_at=now,
    )
    db.add(decision)
    db.flush()
    return claim, decision, pre_review


def _build_session() -> Session:
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
    return factory()
