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
from app.models.financial_connector import (
    FinancialConnectorConfig,
    FinancialConnectorEvent,
    PaymentReconciliationCase,
    PaymentReconciliationEvent,
)
from app.models.financial_record import ExpenseClaim, ExpenseClaimItem
from app.models.savings import SavingsEvidenceLink, SavingsRealization
from app.schemas.financial_connector import (
    FinancialEventEnvelope,
    PaymentReconciliationActionCreate,
)
from app.schemas.savings import SavingsRealizationActionCreate
from app.services.expense_cases import ExpenseCaseService
from app.services.financial_connector_auth import (
    FinancialConnectorAuthError,
    FinancialConnectorSecretResolver,
    sign_financial_event,
)
from app.services.financial_connector_ingestion import (
    FinancialConnectorConflictError,
    FinancialConnectorIngestionService,
)
from app.services.financial_connector_projection import (
    FinancialConnectorProjectionService,
    FinancialReconciliationPermissionError,
)
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_hmac_is_tenant_bound_time_bounded_and_secret_never_persisted(db: Session) -> None:
    _config(db, tenant_id="tenant-a", environment="mock")
    claim = _claim(db, tenant_id="tenant-a")
    envelope = _event(claim, tenant_id="tenant-a", event_id="settled-auth-001")
    timestamp = 1_800_000_000
    signature = sign_financial_event(
        envelope,
        timestamp=timestamp,
        secret="server-only-secret",
        tenant_id="tenant-a",
        provider="mock-bank",
        key_version="v1",
    )

    with pytest.raises(FinancialConnectorAuthError, match="租户"):
        _service(db, timestamp).ingest(
            envelope,
            tenant_header="tenant-b",
            provider_header="mock-bank",
            key_version_header="v1",
            timestamp_header=str(timestamp),
            signature_header=signature,
        )
    with pytest.raises(FinancialConnectorAuthError) as expired:
        _service(db, timestamp + 301).ingest(
            envelope,
            tenant_header="tenant-a",
            provider_header="mock-bank",
            key_version_header="v1",
            timestamp_header=str(timestamp),
            signature_header=signature,
        )
    assert expired.value.code == "timestamp_outside_window"
    with pytest.raises(FinancialConnectorAuthError) as invalid:
        _service(db, timestamp).ingest(
            envelope,
            tenant_header="tenant-a",
            provider_header="mock-bank",
            key_version_header="v1",
            timestamp_header=str(timestamp),
            signature_header="sha256=" + "0" * 64,
        )
    assert invalid.value.code == "signature_invalid"
    config = db.scalar(select(FinancialConnectorConfig))
    assert config is not None
    assert config.secret_ref == "connector/test-key"
    assert "server-only-secret" not in str(config.__dict__)
    with pytest.raises(ValueError, match="未允许字段"):
        FinancialEventEnvelope(
            **{
                **envelope.model_dump(),
                "payload": {**envelope.payload, "bank_account_number": "sensitive"},
            }
        )


def test_hmac_binds_provider_and_key_version_even_when_secret_is_shared(
    db: Session,
) -> None:
    _config(db, tenant_id="tenant-a", provider="mock-bank", key_version="v1")
    _config(db, tenant_id="tenant-a", provider="alias-bank", key_version="v2")
    claim = _claim(db, tenant_id="tenant-a")
    envelope = _event(claim, tenant_id="tenant-a", event_id="source-bound-001")
    timestamp = 1_800_000_000
    signature = sign_financial_event(
        envelope,
        timestamp=timestamp,
        secret="server-only-secret",
        tenant_id="tenant-a",
        provider="mock-bank",
        key_version="v1",
    )
    service = _service(db, timestamp)

    with pytest.raises(FinancialConnectorAuthError) as replayed_to_alias:
        service.ingest(
            envelope,
            tenant_header="tenant-a",
            provider_header="alias-bank",
            key_version_header="v2",
            timestamp_header=str(timestamp),
            signature_header=signature,
        )

    assert replayed_to_alias.value.code == "signature_invalid"


def test_settlement_replay_erp_reversal_and_savings_evidence_e2e(db: Session) -> None:
    _config(db, environment="production")
    claim = _claim(db)
    application = _linked_application(db, claim)
    _savings_opportunity(db, claim)
    db.commit()
    timestamp = 1_800_000_000
    settlement = _event(claim, event_id="settled-e2e-001")

    first = _ingest(db, settlement, timestamp)
    db.commit()
    replay = _ingest(db, settlement, timestamp)
    db.commit()
    db.refresh(claim)
    assert first.replayed is False
    assert replay.replayed is True
    assert first.event_id == replay.event_id
    assert first.processing_status == "processed"
    assert first.verification_level == "production_verified"
    assert first.evidence_classification == "external_cash"
    assert first.projection_scope == "canonical"
    assert claim.status == "paid"
    db.refresh(application)
    assert application.approval_stage == "申请归档"
    assert db.scalar(select(func.count(FinancialConnectorEvent.id))) == 1
    assert db.scalar(select(func.count(PaymentReconciliationEvent.id))) == 1
    connector_fact = db.get(FinancialConnectorEvent, first.event_id)
    assert connector_fact is not None
    assert "claim_reference" not in connector_fact.normalized_payload_json
    assert claim.claim_no not in str(connector_fact.normalized_payload_json)

    realization = db.scalar(
        select(SavingsRealization).where(SavingsRealization.claim_id == claim.id)
    )
    assert realization is not None
    evidence = db.scalar(
        select(SavingsEvidenceLink).where(
            SavingsEvidenceLink.realization_id == realization.id,
            SavingsEvidenceLink.resource_type == "financial_connector_event",
        )
    )
    assert evidence is not None
    assert evidence.resource_id == first.event_id
    assert evidence.source_system == "mock-bank"
    assert evidence.metadata_json["evidence_level"] == "external_cash"
    SavingsRealizationService(db).execute_action(
        realization.id,
        SavingsRealizationActionCreate(
            action="confirm",
            request_id="connector-confirm-001",
            expected_version=1,
            comment="独立财务已核验生产连接器回执与政策基线。",
        ),
        _user("independent-confirmer", roles=["finance"]),
    )

    invalid_erp = _event(
        claim,
        event_id="erp-e2e-invalid-001",
        event_type="erp_posted",
        extra={
            "amount": "65.00",
            "origin_external_event_id": settlement.external_event_id,
            "erp_document_number": "ERP-VOUCHER-INVALID",
        },
    )
    invalid_erp_result = _ingest(db, invalid_erp, timestamp + 1)
    db.commit()
    db.refresh(claim)
    assert invalid_erp_result.processing_status == "exception"
    assert invalid_erp_result.error_code == "erp_amount_mismatch"
    assert claim.status == "paid"
    assert "erp_posted" not in set(db.scalars(select(BusinessEvent.event_type)).all())

    erp = _event(
        claim,
        event_id="erp-e2e-001",
        event_type="erp_posted",
        extra={
            "origin_external_event_id": settlement.external_event_id,
            "erp_document_number": "ERP-VOUCHER-2026-000991",
            "accounting_period": "2026-07",
        },
    )
    erp = FinancialEventEnvelope.model_validate(
        {
            **erp.model_dump(),
            "payload": {
                key: value
                for key, value in erp.payload.items()
                if key != "external_payment_reference"
            },
        }
    )
    erp_result = _ingest(db, erp, timestamp + 2)
    db.commit()
    db.refresh(claim)
    case = db.get(PaymentReconciliationCase, first.reconciliation_case_id)
    assert erp_result.processing_status == "processed"
    assert claim.status == "paid"
    assert case is not None and case.erp_status == "posted"
    assert case.erp_document_tail == "6-000991"
    assert "ERP-VOUCHER" not in str(case.__dict__)

    reversal = _event(
        claim,
        event_id="refund-e2e-001",
        event_type="payment_refunded",
        extra={"origin_external_event_id": settlement.external_event_id},
    )
    reversed_result = _ingest(db, reversal, timestamp + 3)
    db.commit()
    db.refresh(claim)
    db.refresh(case)
    assert reversed_result.processing_status == "processed"
    assert claim.status == "pending_payment"
    assert case.status == "reopened"
    db.refresh(realization)
    reversal_realization = db.scalar(
        select(SavingsRealization).where(
            SavingsRealization.reversal_of_realization_id == realization.id
        )
    )
    assert realization.reversed_at is not None
    assert reversal_realization is not None
    assert reversal_realization.actual_net == -realization.actual_net
    assert any(
        item.get("role") == "external_payment_reversal"
        and item.get("verification_status") == "verified"
        for item in reversal_realization.evidence_json
    )
    event_types = set(db.scalars(select(BusinessEvent.event_type)).all())
    assert {"payment_completed", "erp_posted", "payment_reversed"} <= event_types
    second_settlement = _event(claim, event_id="settled-e2e-002")
    second_result = _ingest(db, second_settlement, timestamp + 4)
    db.commit()
    db.refresh(claim)
    db.refresh(case)
    assert second_result.processing_status == "processed"
    assert claim.status == "paid"
    assert case.status == "matched"
    assert case.erp_status == "pending_posting"
    assert db.scalar(select(func.count(FinancialConnectorEvent.id))) == 5


def test_mismatch_failure_and_conflicting_replay_have_no_payment_side_effect(db: Session) -> None:
    _config(db)
    claim = _claim(db)
    db.commit()
    timestamp = 1_800_000_000
    mismatch = _event(
        claim,
        event_id="settled-mismatch-001",
        extra={"amount": "999.00"},
    )
    result = _ingest(db, mismatch, timestamp)
    db.commit()
    db.refresh(claim)
    assert result.processing_status == "exception"
    assert result.error_code == "amount_mismatch"
    assert claim.status == "pending_payment"
    assert db.scalar(select(func.count(BusinessEvent.id))) == 0

    conflict = _event(
        claim,
        event_id="settled-mismatch-001",
        extra={"amount": "998.00"},
    )
    with pytest.raises(FinancialConnectorConflictError):
        _ingest(db, conflict, timestamp + 1)
    db.rollback()
    db.refresh(claim)
    assert claim.status == "pending_payment"
    assert db.scalar(select(func.count(FinancialConnectorEvent.id))) == 1

    failed = _event(
        claim,
        event_id="payment-failed-001",
        event_type="payment_failed",
        extra={"failure_code": "provider_timeout"},
    )
    failed_result = _ingest(db, failed, timestamp + 2)
    db.commit()
    db.refresh(claim)
    assert failed_result.error_code == "external_payment_failed"
    assert claim.status == "pending_payment"


@pytest.mark.parametrize("environment", ["test", "mock", "staging"])
def test_non_production_events_only_create_simulation_facts(
    db: Session,
    environment: str,
) -> None:
    _config(db, environment=environment)
    claim = _claim(db)
    application = _linked_application(db, claim)
    _savings_opportunity(db, claim)
    db.commit()
    initial_business_event_count = int(
        db.scalar(select(func.count(BusinessEvent.id))) or 0
    )
    timestamp = 1_800_000_000
    settlement = _event(claim, event_id="pending-saving-settled-001")
    settlement_result = _ingest(db, settlement, timestamp)
    db.commit()
    db.refresh(claim)
    db.refresh(application)
    assert settlement_result.processing_status == "processed"
    assert settlement_result.projection_scope == "simulation_only"
    assert settlement_result.reconciliation_case_id is None
    assert settlement_result.claim_status == "pending_payment"
    assert claim.status == "pending_payment"
    assert application.approval_stage == "审批完成"

    failed = _event(
        claim,
        event_id="pending-saving-failed-001",
        event_type="payment_failed",
        extra={"failure_code": "simulated_provider_failure"},
    )
    failed_result = _ingest(db, failed, timestamp + 1)
    db.commit()
    assert failed_result.processing_status == "exception"
    assert failed_result.error_code == "external_payment_failed"
    assert failed_result.projection_scope == "simulation_only"

    erp = _event(
        claim,
        event_id="pending-saving-erp-001",
        event_type="erp_posted",
        extra={
            "origin_external_event_id": settlement.external_event_id,
            "erp_document_number": "ERP-SIMULATED-001",
        },
    )
    erp_result = _ingest(db, erp, timestamp + 2)
    db.commit()
    assert erp_result.processing_status == "processed"
    assert erp_result.projection_scope == "simulation_only"

    erp_failed = _event(
        claim,
        event_id="pending-saving-erp-failed-001",
        event_type="erp_posting_failed",
        extra={
            "origin_external_event_id": settlement.external_event_id,
            "failure_code": "simulated_erp_failure",
        },
    )
    erp_failed_result = _ingest(db, erp_failed, timestamp + 3)
    db.commit()
    assert erp_failed_result.processing_status == "exception"
    assert erp_failed_result.error_code == "erp_posting_failed"
    assert erp_failed_result.projection_scope == "simulation_only"

    refund = _event(
        claim,
        event_id="pending-saving-refund-001",
        event_type="payment_refunded",
        extra={"origin_external_event_id": settlement.external_event_id},
    )
    refund_result = _ingest(db, refund, timestamp + 4)
    db.commit()
    db.refresh(claim)
    assert refund_result.processing_status == "processed"
    assert refund_result.projection_scope == "simulation_only"
    reversal = _event(
        claim,
        event_id="pending-saving-reversal-001",
        event_type="payment_reversed",
        extra={"origin_external_event_id": settlement.external_event_id},
    )
    reversal_result = _ingest(db, reversal, timestamp + 5)
    db.commit()
    assert reversal_result.processing_status == "processed"
    assert reversal_result.projection_scope == "simulation_only"
    assert claim.status == "pending_payment"
    assert db.scalar(select(func.count(PaymentReconciliationCase.id))) == 0
    assert db.scalar(select(func.count(PaymentReconciliationEvent.id))) == 0
    assert db.scalar(select(func.count(BusinessEvent.id))) == initial_business_event_count
    assert db.scalar(select(func.count(SavingsRealization.id))) == 0
    facts = list(db.scalars(select(FinancialConnectorEvent)).all())
    assert len(facts) == 6
    assert all(item.expense_case_id is None for item in facts)
    assert all("claim_reference" not in item.normalized_payload_json for item in facts)


def test_cross_tenant_claim_is_hidden_and_finance_actions_are_separated(db: Session) -> None:
    _config(db, tenant_id="tenant-a")
    _config(db, tenant_id="tenant-b", secret_ref="connector/tenant-b")
    claim = _claim(db, tenant_id="tenant-a", employee_name="申请人甲")
    db.commit()
    timestamp = 1_800_000_000
    cross = _event(claim, tenant_id="tenant-b", event_id="cross-tenant-001")
    signature = sign_financial_event(
        cross,
        timestamp=timestamp,
        secret="tenant-b-secret-strong",
        tenant_id="tenant-b",
        provider="mock-bank",
        key_version="v1",
    )
    result = FinancialConnectorIngestionService(
        db,
        secrets=FinancialConnectorSecretResolver(
            {
                "connector/test-key": "server-only-secret",
                "connector/tenant-b": "tenant-b-secret-strong",
            }
        ),
        now_epoch=timestamp,
    ).ingest(
        cross,
        tenant_header="tenant-b",
        provider_header="mock-bank",
        key_version_header="v1",
        timestamp_header=str(timestamp),
        signature_header=signature,
    )
    db.commit()
    db.refresh(claim)
    assert result.error_code == "claim_not_found"
    assert result.reconciliation_case_id is None
    assert claim.status == "pending_payment"

    finance = _user("finance-a", tenant_id="tenant-a", roles=["finance"])
    outsider = _user("employee-a", tenant_id="tenant-a")
    with pytest.raises(FinancialReconciliationPermissionError):
        FinancialConnectorProjectionService(db).list_cases(
            outsider,
            status_filter=None,
            page=1,
            page_size=20,
        )
    tenant_a_cases = FinancialConnectorProjectionService(db).list_cases(
        finance,
        status_filter=None,
        page=1,
        page_size=20,
    )
    assert tenant_a_cases.total == 0


def test_production_reversal_cannot_reference_simulation_origin(db: Session) -> None:
    _config(db, environment="mock", key_version="v1")
    _config(db, environment="production", key_version="v2")
    claim = _claim(db)
    db.commit()
    timestamp = 1_800_000_000
    simulated = _event(claim, event_id="simulated-origin-001")
    simulated_result = _ingest(db, simulated, timestamp)
    db.commit()
    assert simulated_result.projection_scope == "simulation_only"

    reversal = _event(
        claim,
        event_id="production-reversal-001",
        event_type="payment_reversed",
        extra={"origin_external_event_id": simulated.external_event_id},
    )
    signature = sign_financial_event(
        reversal,
        timestamp=timestamp + 1,
        secret="server-only-secret",
        tenant_id="default",
        provider="mock-bank",
        key_version="v2",
    )
    result = FinancialConnectorIngestionService(
        db,
        secrets=FinancialConnectorSecretResolver(
            {"connector/test-key": "server-only-secret"}
        ),
        now_epoch=timestamp + 1,
    ).ingest(
        reversal,
        tenant_header="default",
        provider_header="mock-bank",
        key_version_header="v2",
        timestamp_header=str(timestamp + 1),
        signature_header=signature,
    )
    db.commit()
    db.refresh(claim)
    assert result.projection_scope == "canonical"
    assert result.error_code == "origin_settlement_not_found"
    assert claim.status == "pending_payment"
    assert result.reconciliation_case_id is None


def test_applicant_cannot_confirm_own_exception(db: Session) -> None:
    _config(db, environment="production")
    employee_id = str(uuid.uuid4())
    claim = _claim(db, employee_id=employee_id, employee_name="申请人甲")
    db.commit()
    mismatch = _event(claim, event_id="own-exception-001", extra={"amount": "2.00"})
    result = _ingest(db, mismatch, 1_800_000_000)
    db.commit()
    case = db.get(PaymentReconciliationCase, result.reconciliation_case_id)
    assert case is not None
    applicant_finance = _user(
        "applicant-finance",
        roles=["finance"],
        employee_id=employee_id,
        name="申请人甲",
    )
    with pytest.raises(FinancialReconciliationPermissionError):
        FinancialConnectorProjectionService(db).resolve(
            case.id,
            PaymentReconciliationActionCreate(
                expected_version=case.version,
                reason="本人尝试确认自己的异常",
            ),
            applicant_finance,
            action="confirmed",
        )
    db.rollback()
    db.refresh(case)
    independent_finance = _user("independent-finance", roles=["finance"])
    resolved = FinancialConnectorProjectionService(db).resolve(
        case.id,
        PaymentReconciliationActionCreate(
            expected_version=case.version,
            reason="独立财务确认该差异仅作为处置结论",
        ),
        independent_finance,
        action="confirmed",
    )
    db.commit()
    db.refresh(claim)
    assert resolved is not None and resolved.status == "confirmed"
    assert claim.status == "pending_payment"


def _service(db: Session, timestamp: int) -> FinancialConnectorIngestionService:
    return FinancialConnectorIngestionService(
        db,
        secrets=FinancialConnectorSecretResolver(
            {"connector/test-key": "server-only-secret"}
        ),
        now_epoch=timestamp,
    )


def _ingest(
    db: Session,
    envelope: FinancialEventEnvelope,
    timestamp: int,
):
    signature = sign_financial_event(
        envelope,
        timestamp=timestamp,
        secret="server-only-secret",
        tenant_id=envelope.tenant_id,
        provider="mock-bank",
        key_version="v1",
    )
    return _service(db, timestamp).ingest(
        envelope,
        tenant_header=envelope.tenant_id,
        provider_header="mock-bank",
        key_version_header="v1",
        timestamp_header=str(timestamp),
        signature_header=signature,
    )


def _config(
    db: Session,
    *,
    tenant_id: str = "default",
    environment: str = "mock",
    secret_ref: str = "connector/test-key",
    provider: str = "mock-bank",
    key_version: str = "v1",
) -> FinancialConnectorConfig:
    row = FinancialConnectorConfig(
        id=str(uuid.uuid4()),
        tenant_id=tenant_id,
        provider=provider,
        environment=environment,
        key_version=key_version,
        secret_ref=secret_ref,
        allowed_event_types_json=[
            "payment_settled",
            "payment_failed",
            "erp_posted",
            "erp_posting_failed",
            "payment_refunded",
            "payment_reversed",
        ],
        clock_skew_seconds=300,
        status="active",
        created_by="platform-admin",
    )
    db.add(row)
    db.flush()
    return row


def _claim(
    db: Session,
    *,
    tenant_id: str = "default",
    employee_id: str | None = None,
    employee_name: str = "测试员工",
) -> ExpenseClaim:
    claim = ExpenseClaim(
        id=str(uuid.uuid4()),
        claim_no=f"BX-CONNECTOR-{uuid.uuid4().hex[:10].upper()}",
        employee_id=employee_id,
        employee_name=employee_name,
        department_name="财务测试部",
        expense_type="travel",
        reason="连接器端到端测试",
        location="上海",
        amount=Decimal("66.00"),
        currency="CNY",
        invoice_count=1,
        occurred_at=datetime.now(UTC),
        submitted_at=datetime.now(UTC),
        status="pending_payment",
        approval_stage="待付款",
        risk_flags_json=[],
    )
    db.add(claim)
    db.flush()
    if tenant_id != "default":
        ExpenseCaseService(db).ensure_case_for_claim(claim, tenant_id=tenant_id)
    return claim


def _event(
    claim: ExpenseClaim,
    *,
    tenant_id: str = "default",
    event_id: str,
    event_type: str = "payment_settled",
    extra: dict[str, str] | None = None,
) -> FinancialEventEnvelope:
    payload = {
        "claim_id": claim.id,
        "claim_reference": claim.claim_no,
        "amount": str(claim.amount),
        "currency": claim.currency,
        "external_payment_reference": f"PAY-{claim.claim_no}-0001",
        **dict(extra or {}),
    }
    return FinancialEventEnvelope(
        tenant_id=tenant_id,
        external_event_id=event_id,
        event_type=event_type,
        occurred_at=datetime.now(UTC),
        correlation_id=f"corr-{event_id}"[:64],
        payload=payload,
    )


def _savings_opportunity(db: Session, claim: ExpenseClaim) -> None:
    item = ExpenseClaimItem(
        id=str(uuid.uuid4()),
        claim=claim,
        item_date=date.today(),
        item_type="hotel",
        item_reason="住宿",
        item_location="上海",
        item_note="",
        item_amount=Decimal("100.00"),
    )
    db.add(item)
    db.flush()
    SavingsDiscoveryService(db).discover_standard_adjustments(
        claim=claim,
        items_by_id={item.id: item},
        adjustment_flags=[
            {
                "item_id": item.id,
                "message": "政策调整",
                "original_amount": "100.00",
                "reimbursable_amount": "66.00",
                "employee_absorbed_amount": "34.00",
                "policy_rule_version": "connector-policy-v1",
                "policy_rule_version_source": "published",
                "policy_grade": "P6",
                "policy_matched_city": "上海",
                "calculation_fingerprint": "sha256:" + "d" * 64,
            }
        ],
        current_user=_user("finance-payer", roles=["finance"]),
        request_id="connector-discovery-001",
    )


def _linked_application(db: Session, reimbursement: ExpenseClaim) -> ExpenseClaim:
    application = ExpenseClaim(
        id=str(uuid.uuid4()),
        claim_no=f"AP-CONNECTOR-{uuid.uuid4().hex[:10].upper()}",
        employee_name=reimbursement.employee_name,
        department_name=reimbursement.department_name,
        expense_type="travel_application",
        reason="连接器端到端申请",
        location=reimbursement.location,
        amount=reimbursement.amount,
        currency=reimbursement.currency,
        invoice_count=0,
        occurred_at=datetime.now(UTC),
        submitted_at=datetime.now(UTC),
        status="approved",
        approval_stage="审批完成",
        risk_flags_json=[],
    )
    db.add(application)
    db.flush()
    reimbursement.risk_flags_json = [
        {
            "source": "application_handoff",
            "event_type": "expense_application_to_reimbursement_draft",
            "application_claim_id": application.id,
            "application_claim_no": application.claim_no,
        }
    ]
    return application


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