from __future__ import annotations

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

import pytest
from sqlalchemy import create_engine
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.financial_record import ExpenseClaim, ExpenseClaimItem
from app.schemas.cfo_value import CfoValueFiltersRead
from app.schemas.savings import (
    SavingsEvidenceCreate,
    SavingsRealizationActionCreate,
    SavingsRealizationCreate,
)
from app.services.cfo_value_analytics import CfoValueAnalyticsService
from app.services.savings_access_policy import SavingsPermissionError
from app.services.savings_discovery import SavingsDiscoveryService
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_cfo_value_counts_only_confirmed_canonical_and_replays_as_of(db: Session) -> None:
    operator = _user("finance-operator", roles=["finance"])
    confirmer = _user("finance-confirmer", roles=["finance"])
    cny_opportunity = _discover(db, operator, currency="CNY", saving=Decimal("200"))
    usd_opportunity = _discover(db, operator, currency="USD", saving=Decimal("50"))
    db.commit()

    cny_realization = (
        SavingsRealizationService(db)
        .record(
            cny_opportunity,
            _record_payload("record-cny-001", Decimal("200"), "CNY"),
            operator,
        )
        .response.realization
    )
    usd_realization = (
        SavingsRealizationService(db)
        .record(
            usd_opportunity,
            _record_payload("record-usd-001", Decimal("50"), "USD"),
            operator,
        )
        .response.realization
    )

    start = datetime.now(UTC) - timedelta(days=2)
    end = datetime.now(UTC) + timedelta(days=2)
    pending_dashboard = CfoValueAnalyticsService(db).build_dashboard(
        confirmer,
        start=start,
        end=end,
        as_of=datetime.now(UTC),
        filters=CfoValueFiltersRead(),
    )
    assert pending_dashboard.kpis.verified_cash.status == "empty"
    assert pending_dashboard.data_quality.pending_confirmation_count == 2
    assert _stage(pending_dashboard, "actual_pending").count == 2

    confirmed_cny = (
        SavingsRealizationService(db)
        .execute_action(
            cny_realization.id,
            SavingsRealizationActionCreate(
                action="confirm",
                request_id="confirm-cny-001",
                expected_version=1,
                comment="独立复核 CNY 结果与归因",
            ),
            confirmer,
        )
        .response
    )
    SavingsRealizationService(db).execute_action(
        usd_realization.id,
        SavingsRealizationActionCreate(
            action="confirm",
            request_id="confirm-usd-001",
            expected_version=1,
            comment="独立复核 USD 结果与归因",
        ),
        confirmer,
    )
    confirmed_as_of = confirmed_cny.event.occurred_at

    current = CfoValueAnalyticsService(db).build_dashboard(
        confirmer,
        start=start,
        end=end,
        as_of=datetime.now(UTC),
        filters=CfoValueFiltersRead(),
    )
    assert _money(current.kpis.verified_cash.values) == {
        "CNY": Decimal("200.0000"),
        "USD": Decimal("50.0000"),
    }
    assert current.kpis.releasable_labor.status == "collecting"
    assert current.kpis.safe_straight_through.status == "collecting"
    assert (
        next(
            item for item in current.guardrails if item.key == "confirmed_high_risk_exposure"
        ).status
        == "unavailable"
    )

    SavingsRealizationService(db).execute_action(
        cny_realization.id,
        SavingsRealizationActionCreate(
            action="reverse",
            request_id="reverse-cny-001",
            expected_version=2,
            comment="补付后全额冲回 CNY 节省",
            reversal_amount=Decimal("200"),
        ),
        confirmer,
    )
    latest = CfoValueAnalyticsService(db).build_dashboard(
        confirmer,
        start=start,
        end=end,
        as_of=datetime.now(UTC),
        filters=CfoValueFiltersRead(),
    )
    assert _money(latest.kpis.verified_cash.values) == {
        "CNY": Decimal("0.0000"),
        "USD": Decimal("50.0000"),
    }
    assert _stage(latest, "reversed").count == 1

    historical = CfoValueAnalyticsService(db).build_dashboard(
        confirmer,
        start=start,
        end=end,
        as_of=confirmed_as_of,
        filters=CfoValueFiltersRead(),
    )
    assert _money(historical.kpis.verified_cash.values)["CNY"] == Decimal("200.0000")

    with pytest.raises(SavingsPermissionError):
        CfoValueAnalyticsService(db).build_dashboard(
            _user("employee"),
            start=start,
            end=end,
            as_of=datetime.now(UTC),
            filters=CfoValueFiltersRead(),
        )


def test_cfo_value_budget_monitor_is_limited_to_own_department(db: Session) -> None:
    operator = _user("finance-operator", roles=["finance"])
    _discover(
        db,
        operator,
        currency="CNY",
        saving=Decimal("100"),
        department_name="销售部",
    )
    _discover(
        db,
        operator,
        currency="CNY",
        saving=Decimal("300"),
        department_name="研发部",
    )
    db.commit()
    dashboard = CfoValueAnalyticsService(db).build_dashboard(
        _user("budget-sales", roles=["budget_monitor"], department_name="销售部"),
        start=datetime.now(UTC) - timedelta(days=1),
        end=datetime.now(UTC) + timedelta(days=1),
        as_of=datetime.now(UTC),
        filters=CfoValueFiltersRead(),
    )
    assert dashboard.source.opportunity_count == 1
    assert _money(_stage(dashboard, "estimated").values) == {
        "CNY": Decimal("100.0000")
    }


def _discover(
    db: Session,
    current_user: CurrentUserContext,
    *,
    currency: str,
    saving: Decimal,
    department_name: str = "销售部",
) -> str:
    original = Decimal("1000")
    target = original - saving
    claim = ExpenseClaim(
        id=str(uuid.uuid4()),
        claim_no=f"BX-{uuid.uuid4().hex[:12]}",
        employee_name="测试员工",
        department_name=department_name,
        project_code="PROJECT-CFO",
        expense_type="hotel",
        reason="客户现场差旅",
        location="上海",
        amount=original,
        currency=currency,
        invoice_count=1,
        occurred_at=datetime.now(UTC),
        status="draft",
        risk_flags_json=[],
    )
    item = ExpenseClaimItem(
        id=str(uuid.uuid4()),
        claim=claim,
        item_date=date.today(),
        item_type="hotel",
        item_reason="住宿",
        item_location="上海",
        item_note="",
        item_amount=original,
    )
    db.add(claim)
    db.flush()
    return (
        SavingsDiscoveryService(db)
        .discover_standard_adjustments(
            claim=claim,
            items_by_id={item.id: item},
            adjustment_flags=[
                {
                    "item_id": item.id,
                    "message": "CFO 聚合测试政策差额",
                    "original_amount": str(original),
                    "reimbursable_amount": str(target),
                    "employee_absorbed_amount": str(saving),
                    "policy_rule_version": f"cfo-{currency.lower()}-v1",
                    "policy_grade": "P6",
                    "policy_matched_city": "上海",
                    "calculation_fingerprint": "sha256:" + uuid.uuid4().hex * 2,
                }
            ],
            current_user=current_user,
            request_id=f"discover-{currency.lower()}-{uuid.uuid4().hex[:8]}",
        )[0]
        .id
    )


def _record_payload(
    request_id: str,
    amount: Decimal,
    currency: str,
) -> SavingsRealizationCreate:
    return SavingsRealizationCreate(
        request_id=request_id,
        expected_version=1,
        comment="登记待独立确认的实际结果",
        actual_gross=amount,
        incremental_cost=Decimal("0"),
        currency=currency,
        realized_at=datetime.now(UTC),
        attribution_method="server_policy_counterfactual",
        attribution_ratio=Decimal("1"),
        evidence_level="business_state",
        evidence=[
            SavingsEvidenceCreate(
                evidence_key=f"evidence-{request_id}",
                evidence_role="payment_business_state",
                resource_type="business_event",
                resource_id=f"payment-{request_id}",
                source_system="x-financial",
                external_event_id=f"payment-{request_id}",
                content_hash="b" * 64,
                occurred_at=datetime.now(UTC),
                verification_status="unverified",
                metadata_json={"source": "cfo-test"},
            )
        ],
    )


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


def _money(items) -> dict[str, Decimal]:
    return {item.currency: item.amount for item in items}


def _stage(dashboard, key: str):
    return next(item for item in dashboard.funnel.stages if item.key == key)
