from __future__ import annotations

import uuid
from datetime import UTC, date, 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

import app.models  # noqa: F401 - 注册完整 metadata
from app.api.deps import CurrentUserContext
from app.db.base_class import Base
from app.models.expense_case import BusinessEvent, ExpenseCase
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.savings import (
    ProfileBaselineSnapshot,
    SavingsEvent,
    SavingsOpportunity,
    SavingsRealization,
)
from app.schemas.savings import (
    SavingsEvidenceCreate,
    SavingsOpportunityActionCreate,
    SavingsRealizationActionCreate,
    SavingsRealizationCreate,
)
from app.services.savings_access_policy import SavingsPermissionError
from app.services.savings_actions import SavingsActionService
from app.services.savings_discovery import SavingsDiscoveryService
from app.services.savings_protocol import SavingsIdempotencyConflictError
from app.services.savings_query import SavingsQueryService
from app.services.savings_realization import SavingsRealizationService


@pytest.fixture()
def db() -> Session:
    engine = create_engine(
        "sqlite+pysqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(engine)
    factory = sessionmaker(bind=engine, expire_on_commit=False)
    with factory() as session:
        yield session
    Base.metadata.drop_all(engine)
    engine.dispose()


def test_query_is_tenant_scoped_and_owner_scoped(db: Session) -> None:
    first = _seed_opportunity(db, tenant_id="tenant-a", owner_id="owner-a")
    _seed_opportunity(
        db,
        tenant_id="tenant-a",
        owner_id="owner-b",
        department_id="D-2",
    )
    _seed_opportunity(db, tenant_id="tenant-b", owner_id="owner-a")
    db.commit()

    finance_result = SavingsQueryService(db).list_opportunities(
        _user("finance-a", tenant_id="tenant-a", roles=["finance"])
    )
    assert finance_result.total == 2
    assert first.id in {item.id for item in finance_result.items}

    owner_result = SavingsQueryService(db).list_opportunities(
        _user("owner-a", tenant_id="tenant-a", employee_id="owner-a")
    )
    assert owner_result.total == 1
    scoped_result = SavingsQueryService(db).list_opportunities(
        _user(
            "budget-a",
            tenant_id="tenant-a",
            roles=["budget_monitor"],
            department_id="D-1",
        )
    )
    assert scoped_result.total == 1
    assert scoped_result.items[0].id == first.id
    assert (
        SavingsQueryService(db).get_opportunity(
            first.id,
            _user("outsider", tenant_id="tenant-b", roles=["finance"]),
        )
        is None
    )


def test_opportunity_action_is_versioned_and_idempotent(db: Session) -> None:
    opportunity = _seed_opportunity(db, status="identified", owner_id="owner-a")
    db.commit()
    owner = _user("owner-a", employee_id="owner-a")
    payload = SavingsOpportunityActionCreate(
        action="accept",
        request_id="accept-request-001",
        expected_version=1,
        comment="确认接受该节省机会",
    )

    first = SavingsActionService(db).execute(opportunity.id, payload, owner)
    replay = SavingsActionService(db).execute(opportunity.id, payload, owner)

    assert first.response.opportunity.status == "accepted"
    assert first.response.opportunity.version == 2
    assert replay.response.replayed is True
    assert db.scalar(
        select(func.count(SavingsEvent.id)).where(
            SavingsEvent.actor_id == "owner-a",
            SavingsEvent.request_id == payload.request_id,
        )
    ) == 1

    with pytest.raises(SavingsIdempotencyConflictError):
        SavingsActionService(db).execute(
            opportunity.id,
            payload.model_copy(update={"comment": "使用同一请求号篡改内容"}),
            owner,
        )


def test_actual_requires_independent_finance_confirmation_and_appends_reversal(
    db: Session,
) -> None:
    opportunity = _seed_opportunity(
        db,
        status="in_progress",
        owner_id="owner-a",
        accepted_at=datetime.now(UTC),
        started_at=datetime.now(UTC),
    )
    db.commit()
    owner = _user("owner-a", employee_id="owner-a")
    record_payload = SavingsRealizationCreate(
        request_id="record-request-001",
        expected_version=1,
        comment="付款完成后登记实际结果",
        actual_gross=Decimal("100.00"),
        incremental_cost=Decimal("10.00"),
        currency="CNY",
        realized_at=datetime.now(UTC),
        attribution_method="server_policy_counterfactual",
        attribution_ratio=Decimal("1"),
        evidence_level="business_state",
        evidence=[_result_evidence("record-request-001")],
    )
    recorded = SavingsRealizationService(db).record(
        opportunity.id,
        record_payload,
        owner,
    )
    realization_id = recorded.response.realization.id
    assert recorded.response.realization.status == "pending_confirmation"
    assert recorded.response.opportunity.status == "realized"

    confirm_payload = SavingsRealizationActionCreate(
        action="confirm",
        request_id="confirm-request-001",
        expected_version=1,
        comment="已复核政策、付款业务状态及归因",
    )
    with pytest.raises(SavingsPermissionError):
        SavingsRealizationService(db).execute_action(
            realization_id,
            confirm_payload,
            owner,
        )
    with pytest.raises(SavingsPermissionError):
        SavingsRealizationService(db).execute_action(
            realization_id,
            confirm_payload,
            _user("platform-admin", is_admin=True),
        )

    confirmer = _user("finance-b", employee_id="finance-b", roles=["finance"])
    confirmed = SavingsRealizationService(db).execute_action(
        realization_id,
        confirm_payload,
        confirmer,
    )
    assert confirmed.response.realization.status == "finance_confirmed"
    assert confirmed.response.realization.dedupe_status == "canonical"
    assert confirmed.response.realization.evidence_json[0]["verification_status"] == "verified"
    assert confirmed.response.opportunity.status == "verified"

    reversed_result = SavingsRealizationService(db).execute_action(
        realization_id,
        SavingsRealizationActionCreate(
            action="reverse",
            request_id="reverse-request-001",
            expected_version=2,
            comment="员工申诉补付，全额冲回原节省",
            reversal_amount=Decimal("90.00"),
        ),
        confirmer,
    )
    original = db.get(SavingsRealization, realization_id)
    reversal = db.get(SavingsRealization, reversed_result.response.realization.id)
    assert original is not None and original.status == "finance_confirmed"
    assert original.reversed_at is not None
    assert reversal is not None and reversal.realization_type == "reversal"
    assert reversal.actual_net == Decimal("-90.0000")
    assert reversed_result.response.opportunity.status == "reversed"


def test_payment_realization_is_idempotent_and_stays_pending_confirmation(
    db: Session,
) -> None:
    opportunity = _seed_opportunity(
        db,
        status="in_progress",
        owner_id="finance",
        accepted_at=datetime.now(UTC),
        started_at=datetime.now(UTC),
    )
    claim = _seed_claim(db, claim_id=opportunity.claim_id)
    payment_event = BusinessEvent(
        id=str(uuid.uuid4()),
        tenant_id="default",
        expense_case_id=opportunity.expense_case_id,
        aggregate_type="expense_claim",
        aggregate_id=claim.id,
        event_type="payment_completed",
        event_version=1,
        idempotency_key="payment-event-001",
        correlation_id="payment-event-001",
        actor_id="payer",
        actor_type="user",
        payload_json={},
        delivery_status="pending",
        occurred_at=datetime.now(UTC),
    )
    db.add(payment_event)
    db.commit()
    payer = _user("payer", employee_id="payer", roles=["finance"])

    first = SavingsRealizationService(db).realize_paid_claim(
        claim,
        payment_event,
        payer,
    )
    second = SavingsRealizationService(db).realize_paid_claim(
        claim,
        payment_event,
        payer,
    )
    db.commit()

    assert len(first) == 1
    assert second == []
    assert first[0].status == "pending_confirmation"
    assert first[0].dedupe_status == "pending_review"
    assert db.scalar(
        select(func.count(SavingsRealization.id)).where(
            SavingsRealization.opportunity_id == opportunity.id
        )
    ) == 1


def test_standard_adjustment_discovery_freezes_baseline_and_evidence(db: Session) -> None:
    claim = _seed_claim(db)
    item = ExpenseClaimItem(
        id=str(uuid.uuid4()),
        claim_id=claim.id,
        item_date=date(2026, 7, 10),
        item_type="hotel",
        item_reason="上海住宿 2 晚",
        item_location="上海",
        item_note="",
        item_amount=Decimal("1200.00"),
    )
    db.add(item)
    db.flush()
    flag = {
        "item_id": item.id,
        "message": "服务端按政策把 1200 元调整为 800 元",
        "original_amount": "1200.00",
        "reimbursable_amount": "800.00",
        "employee_absorbed_amount": "400.00",
        "policy_rule_version": "v1.2.0",
        "policy_rule_version_source": "published",
        "policy_grade": "P6",
        "policy_matched_city": "上海",
        "calculation_fingerprint": "sha256:" + "a" * 64,
    }

    opportunities = SavingsDiscoveryService(db).discover_standard_adjustments(
        claim=claim,
        items_by_id={item.id: item},
        adjustment_flags=[flag],
        current_user=_user("employee-a", employee_id="employee-a"),
        request_id="standard-adjustment-001",
    )
    replay = SavingsDiscoveryService(db).discover_standard_adjustments(
        claim=claim,
        items_by_id={item.id: item},
        adjustment_flags=[flag],
        current_user=_user("employee-a", employee_id="employee-a"),
        request_id="standard-adjustment-001",
    )
    db.commit()

    assert len(opportunities) == 1
    assert replay[0].id == opportunities[0].id
    assert opportunities[0].status == "in_progress"
    assert opportunities[0].estimated_net == Decimal("400.0000")
    assert opportunities[0].baseline_snapshot.policy_version == "v1.2.0"
    assert opportunities[0].baseline_snapshot.baseline_value == Decimal("1200.0000")
    assert opportunities[0].baseline_snapshot.data_quality_status == "complete"
    assert len(opportunities[0].evidence_links) == 1


def _seed_opportunity(
    db: Session,
    *,
    tenant_id: str = "default",
    status: str = "identified",
    owner_id: str = "finance",
    accepted_at: datetime | None = None,
    started_at: datetime | None = None,
    department_id: str = "D-1",
) -> SavingsOpportunity:
    now = datetime.now(UTC)
    case = ExpenseCase(
        id=str(uuid.uuid4()),
        tenant_id=tenant_id,
        case_no=f"CASE-{uuid.uuid4().hex[:12]}",
        scene_code="travel",
        title="节省测试费用事件",
        current_stage="claiming",
        status="active",
        created_at=now,
        updated_at=now,
    )
    baseline = ProfileBaselineSnapshot(
        id=str(uuid.uuid4()),
        tenant_id=tenant_id,
        baseline_key=f"baseline-{uuid.uuid4()}",
        baseline_type="policy_counterfactual",
        dimension_type="expense_claim_item",
        dimension_id=str(uuid.uuid4()),
        metric_key="pre_adjustment_reimbursable_amount",
        unit="currency",
        original_currency="CNY",
        baseline_value=Decimal("100.00"),
        sample_count=1,
        method="test_policy",
        query_fingerprint="sha256:" + uuid.uuid4().hex,
        data_quality_status="complete",
        data_quality_score=Decimal("1"),
        quality_issues_json=[],
        algorithm_version="test-v1",
        policy_version="policy-v1",
        policy_effective_from=date(2026, 1, 1),
        target_resource_type="expense_claim_item",
        target_resource_id=str(uuid.uuid4()),
        frozen_at=now,
        frozen_by="test",
        version=1,
        created_at=now,
    )
    claim_id = str(uuid.uuid4())
    opportunity = SavingsOpportunity(
        id=str(uuid.uuid4()),
        tenant_id=tenant_id,
        opportunity_key=f"opportunity-{uuid.uuid4()}",
        benefit_key=f"benefit-{uuid.uuid4()}",
        expense_case_id=case.id,
        claim_id=claim_id,
        claim_no_snapshot=f"BX-{uuid.uuid4().hex[:8]}",
        source_type="standard_adjustment",
        source_id=str(uuid.uuid4()),
        category="policy_compliance",
        value_kind="cash",
        title="住宿标准重算",
        description="测试机会",
        exposure_amount=Decimal("100.00"),
        baseline_snapshot_id=baseline.id,
        baseline_amount=Decimal("100.00"),
        target_amount=Decimal("0.00"),
        estimated_gross=Decimal("100.00"),
        estimated_cost=Decimal("0.00"),
        estimated_net=Decimal("100.00"),
        estimated_low=Decimal("100.00"),
        estimated_high=Decimal("100.00"),
        confidence=Decimal("1"),
        currency="CNY",
        reporting_currency="CNY",
        attribution_method="server_policy_counterfactual",
        suggested_action="完成付款后登记实际结果",
        owner_id=owner_id,
        owner_name=owner_id,
        owner_role="finance",
        status=status,
        version=1,
        dimension_json={"department_id": department_id, "city": "上海"},
        baseline_snapshot_json={"baseline_value": "100.00"},
        evidence_json=[],
        accepted_at=accepted_at,
        started_at=started_at,
        created_at=now,
        updated_at=now,
    )
    db.add_all([case, baseline, opportunity])
    db.flush()
    return opportunity


def _seed_claim(db: Session, *, claim_id: str | None = None) -> ExpenseClaim:
    now = datetime.now(UTC)
    claim = ExpenseClaim(
        id=claim_id or str(uuid.uuid4()),
        claim_no=f"BX-{uuid.uuid4().hex[:10]}",
        employee_name="测试员工",
        department_name="财务部",
        project_code="P-001",
        expense_type="travel",
        reason="差旅",
        location="上海",
        amount=Decimal("1200.00"),
        currency="CNY",
        invoice_count=1,
        occurred_at=now,
        status="draft",
        risk_flags_json=[],
    )
    db.add(claim)
    db.flush()
    return claim


def _result_evidence(key: str) -> SavingsEvidenceCreate:
    return SavingsEvidenceCreate(
        evidence_key=f"evidence-{key}",
        evidence_role="payment_business_state",
        resource_type="business_event",
        resource_id=f"payment-{key}",
        source_system="x-financial",
        external_event_id=f"payment-{key}",
        content_hash="c" * 64,
        occurred_at=datetime.now(UTC),
        verification_status="unverified",
        metadata_json={"source": "ledger-service-test"},
    )


def _user(
    username: str,
    *,
    tenant_id: str = "default",
    employee_id: str = "",
    roles: list[str] | None = None,
    is_admin: bool = False,
    department_id: str = "",
) -> CurrentUserContext:
    return CurrentUserContext(
        username=username,
        name=username,
        role_codes=list(roles or []),
        is_admin=is_admin,
        tenant_id=tenant_id,
        employee_id=employee_id,
        department_id=department_id,
    )
