from __future__ import annotations

import json
import uuid
from collections.abc import Generator
from datetime import UTC, datetime
from decimal import Decimal

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
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, get_current_user, get_db
from app.api.v1.endpoints.financial_connectors import router
from app.core.config import get_settings
from app.db.base_class import Base
from app.models.expense_case import BusinessEvent
from app.models.financial_connector import (
    FinancialConnectorConfig,
    FinancialConnectorEvent,
    PaymentReconciliationCase,
)
from app.models.financial_record import ExpenseClaim
from app.models.savings import SavingsRealization
from app.schemas.financial_connector import FinancialConnectorSimulationCreate
from app.services.expense_cases import ExpenseCaseService
from app.services.financial_connector_auth import FinancialConnectorSecretResolver
from app.services.financial_connector_mock_adapter import (
    FinancialConnectorMockAdapter,
    FinancialConnectorMockAdapterError,
)
from app.services.financial_connector_observability import (
    FinancialConnectorObservabilityPermissionError,
    FinancialConnectorObservabilityService,
)
from app.services.financial_connector_payment_evidence import (
    FinancialConnectorPaymentEvidenceService,
)


@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_mock_adapter_covers_deterministic_scenarios_without_core_side_effects(
    db: Session,
) -> None:
    config = _config(db)
    _config(db, provider=config.provider, key_version="v0", status="rotating")
    claim = _claim(db)
    db.commit()
    adapter = FinancialConnectorMockAdapter(
        db,
        secrets=FinancialConnectorSecretResolver(
            {"connector/runtime-test": "runtime-server-only-secret"}
        ),
        now=datetime(2026, 7, 16, 12, 0, tzinfo=UTC),
    )
    expected_outcomes = {
        "success": ["accepted"],
        "failure": ["expected_exception"],
        "out_of_order": ["expected_exception"],
        "duplicate": ["accepted", "replayed"],
        "conflict": ["accepted", "conflict"],
        "refund": ["accepted", "accepted"],
        "erp_receipt": ["accepted", "accepted"],
    }

    results = {}
    for scenario, outcomes in expected_outcomes.items():
        result = adapter.run(
            tenant_id="default",
            config_id=config.id,
            payload=FinancialConnectorSimulationCreate(
                claim_id=claim.id,
                scenario=scenario,
                request_id=f"runtime-{scenario}-request-001",
            ),
        )
        db.commit()
        results[scenario] = result
        assert [step.outcome for step in result.steps] == outcomes
        assert result.projection_scope == "simulation_only"
        assert result.core_side_effects_allowed is False
        serialized = result.model_dump_json().lower()
        assert '"payload":' not in serialized
        assert '"signature":' not in serialized

    repeated = adapter.run(
        tenant_id="default",
        config_id=config.id,
        payload=FinancialConnectorSimulationCreate(
            claim_id=claim.id,
            scenario="success",
            request_id="runtime-success-request-001",
        ),
    )
    db.commit()

    assert repeated.request_fingerprint == results["success"].request_fingerprint
    assert repeated.steps[0].outcome == "replayed"
    assert db.scalar(select(func.count(FinancialConnectorEvent.id))) == 9
    assert db.scalar(select(func.count(PaymentReconciliationCase.id))) == 0
    assert db.scalar(select(func.count(BusinessEvent.id))) == 0
    assert db.scalar(select(func.count(SavingsRealization.id))) == 0
    db.refresh(claim)
    assert claim.status == "pending_payment"
    assert claim.approval_stage == "待付款"
    assert claim.risk_flags_json == []
    assert all(
        event.response_json["projection_scope"] == "simulation_only"
        for event in db.scalars(select(FinancialConnectorEvent)).all()
    )


def test_mock_adapter_rejects_cross_tenant_production_and_inactive_configs(
    db: Session,
) -> None:
    tenant_a = _config(db, tenant_id="tenant-a", provider="tenant-a-bank")
    tenant_b_claim = _claim(db, tenant_id="tenant-b")
    production = _config(db, provider="production-bank", environment="production")
    inactive = _config(db, provider="disabled-bank", status="disabled")
    db.commit()
    adapter = FinancialConnectorMockAdapter(
        db,
        secrets=FinancialConnectorSecretResolver(
            {"connector/runtime-test": "runtime-server-only-secret"}
        ),
    )
    payload = FinancialConnectorSimulationCreate(
        claim_id=tenant_b_claim.id,
        scenario="success",
        request_id="runtime-boundary-request-001",
    )

    with pytest.raises(LookupError, match="报销单不存在"):
        adapter.run(tenant_id="tenant-a", config_id=tenant_a.id, payload=payload)
    with pytest.raises(FinancialConnectorMockAdapterError, match="生产"):
        adapter.run(tenant_id="default", config_id=production.id, payload=payload)
    with pytest.raises(FinancialConnectorMockAdapterError, match="激活"):
        adapter.run(tenant_id="default", config_id=inactive.id, payload=payload)


def test_observability_aggregates_real_facts_and_durable_zero_metrics(
    db: Session,
) -> None:
    config = _config(db)
    claim = _claim(db)
    adapter = FinancialConnectorMockAdapter(
        db,
        secrets=FinancialConnectorSecretResolver(
            {"connector/runtime-test": "runtime-server-only-secret"}
        ),
        now=datetime(2026, 7, 16, 12, 0, tzinfo=UTC),
    )
    for scenario in ("success", "failure", "out_of_order"):
        adapter.run(
            tenant_id="default",
            config_id=config.id,
            payload=FinancialConnectorSimulationCreate(
                claim_id=claim.id,
                scenario=scenario,
                request_id=f"runtime-observe-{scenario}-001",
            ),
        )
    db.add(
        PaymentReconciliationCase(
            id=str(uuid.uuid4()),
            tenant_id="default",
            provider=config.provider,
            claim_id=claim.id,
            expected_amount=claim.amount,
            actual_amount=claim.amount,
            amount_difference=Decimal("0.00"),
            expected_currency="CNY",
            actual_currency="CNY",
            expected_reference=claim.claim_no,
            status="exception",
            exception_code="test_anomaly",
            erp_status="pending_posting",
            last_connector_event_id="test-observability-event",
            version=1,
            created_at=datetime(2026, 7, 16, 12, 0, tzinfo=UTC),
            updated_at=datetime(2026, 7, 16, 12, 0, tzinfo=UTC),
        )
    )
    db.commit()

    service = FinancialConnectorObservabilityService(
        db,
        now=datetime(2026, 7, 16, 12, 5, tzinfo=UTC),
    )
    result = service.read_for_current_user(_user("finance", roles=["finance"]), window_hours=24)
    other = service.read_for_tenant("tenant-other", window_hours=24)

    assert result.summary.event_count == 3
    assert result.summary.processed_event_count == 1
    assert result.summary.failed_event_count == 2
    assert result.summary.failure_rate == pytest.approx(0.6667)
    assert result.summary.backlog_count == 0
    assert result.summary.reconciliation_anomaly_count == 1
    assert result.summary.retry_count == 0
    assert result.summary.auth_failure_count == 0
    assert result.summary.signature_failure_count == 0
    assert result.summary.payload_conflict_count == 0
    assert result.retry_metric.status == "available"
    assert result.auth_failure_metric.status == "available"
    assert result.signature_failure_metric.status == "available"
    assert result.payload_conflict_metric.status == "available"
    assert result.source_revision == "20260716_0022"
    assert result.as_of == datetime(2026, 7, 16, 12, 5, tzinfo=UTC)
    assert all(item.evidence_classification == "simulated_connector" for item in result.items)
    assert sum(item.reconciliation_anomaly_count for item in result.items) == 1
    assert other.summary.event_count == 0
    assert other.items == []
    with pytest.raises(FinancialConnectorObservabilityPermissionError):
        service.read_for_current_user(_user("ordinary"), window_hours=24)


def test_payment_evidence_distinguishes_manual_external_and_unpaid() -> None:
    manual = _claim_object(status="paid")
    manual.risk_flags_json = [
        {
            "source": "payment",
            "event_type": "expense_claim_payment_completed",
            "created_at": "2026-07-16T10:00:00+00:00",
        }
    ]
    external = _claim_object(status="paid")
    external.risk_flags_json = [
        {
            "source": "external_payment",
            "event_type": "expense_claim_external_payment_settled",
            "provider": "verified-bank",
            "verification_level": "production_verified",
            "evidence_classification": "external_cash",
            "external_reference_tail": "12345678",
            "created_at": "2026-07-16T11:00:00+00:00",
        }
    ]
    unpaid = _claim_object(status="pending_payment")

    manual_read = FinancialConnectorPaymentEvidenceService.read(manual)
    external_read = FinancialConnectorPaymentEvidenceService.read(external)
    unpaid_read = FinancialConnectorPaymentEvidenceService.read(unpaid)

    assert manual_read.evidence_classification == "internal_manual_payment"
    assert manual_read.trust_level == "low"
    assert external_read.evidence_classification == "external_cash"
    assert external_read.trust_level == "high"
    assert external_read.external_reference_tail == "12345678"
    assert unpaid_read.evidence_classification == "none"


@pytest.fixture()
def http_context(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setenv(
        "FINANCIAL_CONNECTOR_HMAC_KEYS_JSON",
        json.dumps({"connector/runtime-test": "runtime-server-only-secret"}),
    )
    get_settings.cache_clear()
    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:
        config = _config(session)
        claim = _claim(session)
        claim.status = "paid"
        claim.approval_stage = "已付款"
        claim.risk_flags_json = [
            {
                "source": "payment",
                "event_type": "expense_claim_payment_completed",
                "created_at": "2026-07-16T10:00:00+00:00",
            }
        ]
        session.commit()
    app = FastAPI()
    app.include_router(router, prefix="/api/v1")
    user_box = {"current": _user("platform-admin", is_admin=True)}

    def override_db() -> Generator[Session, None, None]:
        with factory() as session:
            yield session

    app.dependency_overrides[get_db] = override_db
    app.dependency_overrides[get_current_user] = lambda: user_box["current"]
    client = TestClient(app)
    try:
        yield client, user_box, config.id, claim.id
    finally:
        client.close()
        app.dependency_overrides.clear()
        Base.metadata.drop_all(engine)
        engine.dispose()
        get_settings.cache_clear()


def test_runtime_http_permissions_tenant_scope_and_replay(http_context) -> None:
    client, user_box, config_id, claim_id = http_context
    simulation = client.post(
        f"/api/v1/financial-connectors/admin/tenants/default/configs/{config_id}/simulate",
        json={
            "claim_id": claim_id,
            "scenario": "success",
            "request_id": "runtime-http-simulation-001",
        },
    )
    replay = client.post(
        f"/api/v1/financial-connectors/admin/tenants/default/configs/{config_id}/simulate",
        json={
            "claim_id": claim_id,
            "scenario": "success",
            "request_id": "runtime-http-simulation-001",
        },
    )
    assert simulation.status_code == 200
    assert simulation.json()["core_side_effects_allowed"] is False
    assert replay.json()["steps"][0]["outcome"] == "replayed"

    user_box["current"] = _user("finance", roles=["finance"])
    observed = client.get("/api/v1/financial-connectors/observability?window_hours=24")
    evidence = client.get(f"/api/v1/financial-connectors/payment-evidence/{claim_id}")
    assert observed.status_code == 200
    assert observed.json()["summary"]["event_count"] == 1
    assert observed.json()["summary"]["retry_count"] == 1
    assert observed.json()["retry_metric"]["status"] == "available"
    assert observed.json()["source_revision"] == "20260716_0022"
    assert evidence.status_code == 200
    assert evidence.json()["evidence_classification"] == "internal_manual_payment"

    forbidden_simulation = client.post(
        f"/api/v1/financial-connectors/admin/tenants/default/configs/{config_id}/simulate",
        json={
            "claim_id": claim_id,
            "scenario": "success",
            "request_id": "runtime-http-forbidden-001",
        },
    )
    assert forbidden_simulation.status_code == 403

    user_box["current"] = _user("other-finance", tenant_id="tenant-other", roles=["finance"])
    assert (
        client.get(f"/api/v1/financial-connectors/payment-evidence/{claim_id}").status_code == 404
    )
    assert (
        client.get("/api/v1/financial-connectors/observability").json()["tenant_id"]
        == "tenant-other"
    )

    user_box["current"] = _user("ordinary")
    assert client.get("/api/v1/financial-connectors/observability").status_code == 403


def _config(
    db: Session,
    *,
    tenant_id: str = "default",
    provider: str = "runtime-bank",
    environment: str = "mock",
    status: str = "active",
    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="connector/runtime-test",
        allowed_event_types_json=[
            "payment_settled",
            "payment_failed",
            "erp_posted",
            "erp_posting_failed",
            "payment_refunded",
            "payment_reversed",
        ],
        clock_skew_seconds=300,
        status=status,
        created_by="platform-admin",
    )
    db.add(row)
    db.flush()
    return row


def _claim(db: Session, *, tenant_id: str = "default") -> ExpenseClaim:
    row = _claim_object()
    db.add(row)
    db.flush()
    if tenant_id != "default":
        ExpenseCaseService(db).ensure_case_for_claim(row, tenant_id=tenant_id)
    return row


def _claim_object(*, status: str = "pending_payment") -> ExpenseClaim:
    return ExpenseClaim(
        id=str(uuid.uuid4()),
        claim_no=f"BX-RUNTIME-{uuid.uuid4().hex[:10].upper()}",
        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=status,
        approval_stage="待付款" if status == "pending_payment" else "已付款",
        risk_flags_json=[],
    )


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