from __future__ import annotations

from typing import Any

import pytest
from sqlalchemy import inspect, text
from sqlalchemy.engine import Engine
from sqlalchemy.exc import DBAPIError, IntegrityError

from app.models.financial_connector import (
    FinancialConnectorConfig,
    FinancialConnectorConfigEvent,
    FinancialConnectorEvent,
    FinancialConnectorOperationalEvent,
    PaymentReconciliationCase,
    PaymentReconciliationEvent,
)

FINANCIAL_CONNECTOR_MODELS = (
    FinancialConnectorConfig,
    FinancialConnectorConfigEvent,
    FinancialConnectorEvent,
    FinancialConnectorOperationalEvent,
    PaymentReconciliationCase,
    PaymentReconciliationEvent,
)


def _assert_financial_connector_head_schema(engine: Engine) -> None:
    inspector = inspect(engine)
    for model in FINANCIAL_CONNECTOR_MODELS:
        table = model.__table__
        live_columns = {
            str(column["name"]): bool(column["nullable"])
            for column in inspector.get_columns(table.name, schema="public")
        }
        declared_columns = {column.name: bool(column.nullable) for column in table.columns}
        assert live_columns == declared_columns

        live_constraint_names = {
            str(item["name"])
            for loader in (
                inspector.get_unique_constraints,
                inspector.get_check_constraints,
                inspector.get_foreign_keys,
            )
            for item in loader(table.name, schema="public")
            if item.get("name")
        }
        declared_constraint_names = {
            str(constraint.name)
            for constraint in table.constraints
            if constraint.name is not None
        }
        assert live_constraint_names == declared_constraint_names

        live_index_names = {
            str(item["name"])
            for item in inspector.get_indexes(table.name, schema="public")
            if not item.get("duplicates_constraint")
        }
        declared_index_names = {str(index.name) for index in table.indexes}
        assert live_index_names == declared_index_names

    for table_name in (
        "financial_connector_config_events",
        "financial_connector_events",
        "financial_connector_operational_events",
        "payment_reconciliation_events",
    ):
        with engine.connect() as connection:
            trigger_count = int(
                connection.scalar(
                    text(
                        "SELECT COUNT(*) FROM pg_trigger trigger "
                        "JOIN pg_class relation ON relation.oid = trigger.tgrelid "
                        "WHERE relation.relname = :table_name "
                        "AND trigger.tgname = :trigger_name "
                        "AND NOT trigger.tgisinternal"
                    ),
                    {
                        "table_name": table_name,
                        "trigger_name": f"trg_{table_name}_append_only",
                    },
                )
                or 0
            )
        assert trigger_count == 1


def _assert_financial_connector_runtime_invariants(engine: Engine) -> None:
    def rejected(
        connection: Any,
        statement: Any,
        parameters: dict[str, Any],
        error_type: type[DBAPIError] = IntegrityError,
    ) -> None:
        savepoint = connection.begin_nested()
        try:
            with pytest.raises(error_type):
                connection.execute(statement, parameters)
        finally:
            if savepoint.is_active:
                savepoint.rollback()

    config_insert = text(
        """
        INSERT INTO financial_connector_configs (
            id, tenant_id, provider, environment, key_version, secret_ref,
            allowed_event_types_json, clock_skew_seconds, status, version, created_by
        ) VALUES (
            :id, :tenant_id, 'probe-bank', 'production', 'v1', 'server/probe',
            '["payment_settled"]', 300, 'active', 1, 'migration-probe'
        )
        """
    )
    event_insert = text(
        """
        INSERT INTO financial_connector_events (
            id, tenant_id, config_id, provider, environment, direction,
            external_event_id, event_type, occurred_at, key_version,
            verification_level, request_fingerprint, content_hash,
            processing_status, claim_id, expense_case_id, correlation_id,
            normalized_payload_json, response_json
        ) VALUES (
            :id, :tenant_id, :config_id, 'probe-bank', 'production', 'inbound',
            :external_event_id, 'payment_settled', now(), 'v1',
            'production_verified', :fingerprint, :content_hash,
            'processed', 'claim-soft-ref', :expense_case_id, 'probe-correlation',
            '{}', '{}'
        )
        """
    )
    with engine.connect() as connection:
        transaction = connection.begin()
        try:
            connection.execute(
                text(
                    """
                    INSERT INTO expense_cases (
                        id, tenant_id, case_no, scene_code, title, current_stage, status
                    ) VALUES (
                        'connector-case-a', 'tenant-a', 'CASE-CONNECTOR-A',
                        'travel', '连接器迁移探针', 'paying', 'active'
                    )
                    """
                )
            )
            connection.execute(
                config_insert,
                {"id": "connector-config-a", "tenant_id": "tenant-a"},
            )
            connection.execute(
                text(
                    """
                    INSERT INTO financial_connector_operational_events (
                        id, tenant_id, config_id, provider, environment,
                        event_type, reason_code, request_fingerprint,
                        external_event_fingerprint, idempotency_key
                    ) VALUES (
                        'connector-operational-event-a', 'tenant-a',
                        'connector-config-a', 'probe-bank', 'production',
                        'replay', 'duplicate_external_event',
                        :request_fingerprint, :external_event_fingerprint,
                        :idempotency_key
                    )
                    """
                ),
                {
                    "request_fingerprint": "hmac-sha256:" + "a" * 64,
                    "external_event_fingerprint": "hmac-sha256:" + "b" * 64,
                    "idempotency_key": "sha256:" + "c" * 64,
                },
            )
            rejected(
                connection,
                text(
                    """
                    INSERT INTO financial_connector_operational_events (
                        id, tenant_id, config_id, provider, environment,
                        event_type, reason_code, request_fingerprint,
                        external_event_fingerprint, idempotency_key
                    ) VALUES (
                        'connector-operational-event-cross', 'tenant-b',
                        'connector-config-a', 'probe-bank', 'production',
                        'replay', 'duplicate_external_event',
                        :request_fingerprint, :external_event_fingerprint,
                        :idempotency_key
                    )
                    """
                ),
                {
                    "request_fingerprint": "hmac-sha256:" + "d" * 64,
                    "external_event_fingerprint": "hmac-sha256:" + "e" * 64,
                    "idempotency_key": "sha256:" + "f" * 64,
                },
            )
            rejected(
                connection,
                text(
                    """
                    INSERT INTO financial_connector_operational_events (
                        id, tenant_id, config_id, provider, environment,
                        event_type, reason_code, request_fingerprint,
                        external_event_fingerprint, idempotency_key
                    ) VALUES (
                        'connector-operational-weak', 'tenant-a',
                        'connector-config-a', 'probe-bank', 'production',
                        'auth_failure', 'signature_invalid',
                        :request_fingerprint, :external_event_fingerprint,
                        :idempotency_key
                    )
                    """
                ),
                {
                    "request_fingerprint": "sha256:" + "a" * 64,
                    "external_event_fingerprint": "hmac-sha256:" + "b" * 64,
                    "idempotency_key": "sha256:" + "d" * 64,
                },
            )
            connection.execute(
                text(
                    """
                    INSERT INTO financial_connector_config_events (
                        id, tenant_id, config_id, action, actor_id, request_id,
                        reason, expected_version, before_json, after_json
                    ) VALUES (
                        'connector-config-event-a', 'tenant-a', 'connector-config-a',
                        'activated', 'migration-probe', 'migration-request-001',
                        '迁移运行时约束探针', 1, '{}',
                        '{"status": "active", "version": 2}'
                    )
                    """
                )
            )
            rejected(
                connection,
                text(
                    "UPDATE financial_connector_configs SET version = 0 "
                    "WHERE id = 'connector-config-a'"
                ),
                {},
            )
            connection.execute(
                event_insert,
                {
                    "id": "connector-event-a",
                    "tenant_id": "tenant-a",
                    "config_id": "connector-config-a",
                    "external_event_id": "external-a",
                    "fingerprint": "sha256:" + "a" * 64,
                    "content_hash": "sha256:" + "b" * 64,
                    "expense_case_id": "connector-case-a",
                },
            )
            rejected(
                connection,
                event_insert,
                {
                    "id": "connector-event-cross",
                    "tenant_id": "tenant-b",
                    "config_id": "connector-config-a",
                    "external_event_id": "external-cross",
                    "fingerprint": "sha256:" + "c" * 64,
                    "content_hash": "sha256:" + "d" * 64,
                    "expense_case_id": "connector-case-a",
                },
            )
            connection.execute(
                text(
                    """
                    INSERT INTO payment_reconciliation_cases (
                        id, tenant_id, provider, claim_id, expense_case_id,
                        expected_amount, actual_amount, amount_difference,
                        expected_currency, actual_currency, expected_reference,
                        status, erp_status, last_connector_event_id, version
                    ) VALUES (
                        'reconciliation-a', 'tenant-a', 'probe-bank', 'claim-soft-ref',
                        'connector-case-a', 10, 10, 0, 'CNY', 'CNY', 'BX-PROBE',
                        'matched', 'pending_posting', 'connector-event-a', 1
                    )
                    """
                )
            )
            connection.execute(
                text(
                    """
                    INSERT INTO payment_reconciliation_events (
                        id, tenant_id, reconciliation_case_id, connector_event_id,
                        action, actor_type, actor_id, request_fingerprint,
                        before_json, after_json, response_json, correlation_id
                    ) VALUES (
                        'reconciliation-event-a', 'tenant-a', 'reconciliation-a',
                        'connector-event-a', 'auto_matched', 'connector', 'probe-bank',
                        :fingerprint, '{}', '{}', '{}', 'probe-correlation'
                    )
                    """
                ),
                {"fingerprint": "sha256:" + "e" * 64},
            )
            rejected(
                connection,
                text(
                    "UPDATE financial_connector_config_events SET reason = 'tampered' "
                    "WHERE id = 'connector-config-event-a'"
                ),
                {},
                DBAPIError,
            )
            rejected(
                connection,
                text(
                    "UPDATE financial_connector_events SET error_code = 'tampered' "
                    "WHERE id = 'connector-event-a'"
                ),
                {},
                DBAPIError,
            )
            rejected(
                connection,
                text(
                    "UPDATE financial_connector_operational_events "
                    "SET reason_code = 'tampered' "
                    "WHERE id = 'connector-operational-event-a'"
                ),
                {},
                DBAPIError,
            )
            rejected(
                connection,
                text(
                    "DELETE FROM payment_reconciliation_events "
                    "WHERE id = 'reconciliation-event-a'"
                ),
                {},
                DBAPIError,
            )
        finally:
            transaction.rollback()
