from __future__ import annotations

import uuid
from datetime import UTC, datetime, timedelta

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.db.base_class import Base
from app.models.financial_connector import (
    FinancialConnectorConfig,
    FinancialConnectorOperationalEvent,
)
from app.schemas.financial_connector import FinancialEventEnvelope
from app.services.financial_connector_auth import (
    FinancialConnectorAuthenticator,
    FinancialConnectorAuthError,
    FinancialConnectorSecretResolver,
)
from app.services.financial_connector_observability import (
    FinancialConnectorObservabilityService,
)
from app.services.financial_connector_operational_events import (
    FinancialConnectorOperationalContext,
    FinancialConnectorOperationalEventService,
    operational_event_candidate,
)


@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_operational_attempts_are_counted_but_candidate_retry_is_idempotent(
    db: Session,
) -> None:
    config = _config(db)
    context = _context(config)
    as_of = datetime(2026, 7, 17, 10, 2, tzinfo=UTC)
    first = operational_event_candidate(
        context,
        event_type="replay",
        reason_code="duplicate_external_event",
        occurred_at=as_of - timedelta(minutes=2),
    )
    second = operational_event_candidate(
        context,
        event_type="replay",
        reason_code="duplicate_external_event",
        occurred_at=as_of - timedelta(minutes=1),
    )
    outside_window = operational_event_candidate(
        context,
        event_type="auth_failure",
        reason_code="signature_invalid",
        occurred_at=as_of - timedelta(hours=2),
    )
    service = FinancialConnectorOperationalEventService(db)

    service.record(first)
    service.record(first)
    service.record(second)
    service.record(outside_window)
    db.commit()

    assert first.idempotency_key != second.idempotency_key
    assert db.scalar(select(func.count(FinancialConnectorOperationalEvent.id))) == 3
    observed = FinancialConnectorObservabilityService(db, now=as_of).read_for_tenant(
        config.tenant_id,
        window_hours=1,
    )
    assert observed.window_started_at == as_of - timedelta(hours=1)
    assert observed.as_of == as_of
    assert observed.source_revision == "20260716_0022"
    assert observed.summary.retry_count == 2
    assert observed.summary.auth_failure_count == 0
    assert observed.summary.latest_replay_at == second.occurred_at


def test_auth_failure_is_attributed_only_after_trusted_config_and_secret_resolution(
    db: Session,
) -> None:
    config = _config(db)
    secret = "trusted-server-secret"
    now_epoch = 1_800_000_000
    envelope = FinancialEventEnvelope(
        tenant_id=config.tenant_id,
        external_event_id="raw-external-event-must-not-be-stored",
        event_type="payment_settled",
        occurred_at=datetime.fromtimestamp(now_epoch, UTC),
        correlation_id="raw-correlation-must-not-be-stored",
        payload={
            "claim_id": str(uuid.uuid4()),
            "claim_reference": "BX-RAW-MUST-NOT-BE-STORED",
            "amount": "66.00",
            "currency": "CNY",
            "external_payment_reference": "RAW-PAYMENT-MUST-NOT-BE-STORED",
        },
    )
    authenticator = FinancialConnectorAuthenticator(
        db,
        secrets=FinancialConnectorSecretResolver({config.secret_ref: secret}),
        now_epoch=now_epoch,
    )

    with pytest.raises(FinancialConnectorAuthError) as forged_tenant:
        authenticator.verify(
            envelope,
            tenant_header="tenant-forged",
            provider_header=config.provider,
            key_version_header=config.key_version,
            timestamp_header=str(now_epoch),
            signature_header="sha256=" + "0" * 64,
        )
    assert forged_tenant.value.operational_context is None

    with pytest.raises(FinancialConnectorAuthError) as forged_provider:
        authenticator.verify(
            envelope,
            tenant_header=config.tenant_id,
            provider_header="provider-forged",
            key_version_header=config.key_version,
            timestamp_header=str(now_epoch),
            signature_header="sha256=" + "0" * 64,
        )
    assert forged_provider.value.operational_context is None

    with pytest.raises(FinancialConnectorAuthError) as trusted_failure:
        authenticator.verify(
            envelope,
            tenant_header=config.tenant_id,
            provider_header=config.provider,
            key_version_header=config.key_version,
            timestamp_header=str(now_epoch),
            signature_header="sha256=" + "0" * 64,
        )
    context = trusted_failure.value.operational_context
    assert context is not None
    assert context.request_fingerprint.startswith("hmac-sha256:")
    assert context.external_event_fingerprint.startswith("hmac-sha256:")

    candidate = operational_event_candidate(
        context,
        event_type="auth_failure",
        reason_code=trusted_failure.value.code,
        occurred_at=datetime.fromtimestamp(now_epoch, UTC),
    )
    FinancialConnectorOperationalEventService(db).record(candidate)
    db.commit()
    row = db.scalar(select(FinancialConnectorOperationalEvent))
    assert row is not None
    stored = "|".join(
        (
            row.provider,
            row.reason_code,
            row.request_fingerprint,
            row.external_event_fingerprint,
            row.idempotency_key,
        )
    )
    for raw_value in (
        secret,
        envelope.external_event_id,
        envelope.correlation_id,
        envelope.payload["claim_reference"],
        envelope.payload["external_payment_reference"],
    ):
        assert str(raw_value) not in stored


def _config(db: Session) -> FinancialConnectorConfig:
    row = FinancialConnectorConfig(
        id=str(uuid.uuid4()),
        tenant_id="tenant-operational",
        provider="operational-bank",
        environment="production",
        key_version="v1",
        secret_ref="connector/operational-test",
        allowed_event_types_json=["payment_settled"],
        clock_skew_seconds=300,
        status="active",
        created_by="operational-test",
    )
    db.add(row)
    db.flush()
    return row


def _context(config: FinancialConnectorConfig) -> FinancialConnectorOperationalContext:
    return FinancialConnectorOperationalContext(
        tenant_id=config.tenant_id,
        config_id=config.id,
        provider=config.provider,
        environment=config.environment,
        request_fingerprint="hmac-sha256:" + "a" * 64,
        external_event_fingerprint="hmac-sha256:" + "b" * 64,
    )
