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.agent_asset_release_telemetry import (
    AgentAssetReleaseAuditSample,
    AgentAssetReleaseLabel,
    AgentAssetReleaseObservation,
)

RELEASE_TELEMETRY_MODELS = (
    AgentAssetReleaseObservation,
    AgentAssetReleaseLabel,
    AgentAssetReleaseAuditSample,
)


def _assert_release_telemetry_head_schema(engine: Engine) -> None:
    inspector = inspect(engine)
    for model in RELEASE_TELEMETRY_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

    label_foreign_keys = inspector.get_foreign_keys(
        "agent_asset_release_labels",
        schema="public",
    )
    assert len(label_foreign_keys) == 1
    foreign_key = label_foreign_keys[0]
    assert tuple(foreign_key["constrained_columns"]) == (
        "tenant_id",
        "observation_id",
        "asset_id",
        "release_id",
        "stage",
        "version",
    )
    assert foreign_key["referred_table"] == "agent_asset_release_observations"
    assert str(foreign_key.get("options", {}).get("ondelete", "")).upper() == "RESTRICT"

    audit_sample_foreign_keys = inspector.get_foreign_keys(
        "agent_asset_release_audit_samples",
        schema="public",
    )
    assert len(audit_sample_foreign_keys) == 1
    audit_sample_foreign_key = audit_sample_foreign_keys[0]
    assert tuple(audit_sample_foreign_key["constrained_columns"]) == (
        "tenant_id",
        "observation_id",
        "asset_id",
        "release_id",
        "stage",
        "version",
    )
    assert audit_sample_foreign_key["referred_table"] == (
        "agent_asset_release_observations"
    )
    assert (
        str(audit_sample_foreign_key.get("options", {}).get("ondelete", "")).upper()
        == "RESTRICT"
    )

    with engine.connect() as connection:
        trigger_counts = {
            table_name: 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
            )
            for table_name in (
                "agent_asset_release_observations",
                "agent_asset_release_labels",
                "agent_asset_release_audit_samples",
            )
        }
    assert trigger_counts == {
        "agent_asset_release_observations": 1,
        "agent_asset_release_labels": 1,
        "agent_asset_release_audit_samples": 1,
    }


def _assert_release_telemetry_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()

    observation_insert = text(
        """
        INSERT INTO agent_asset_release_observations (
            id, tenant_id, asset_id, release_id, stage, version, rule_code,
            business_stage, source_fingerprint, candidate_hit, baseline_hit,
            runtime_status, idempotency_key, payload_fingerprint
        ) VALUES (
            :id, :tenant_id, 'asset-a', 'release-a', 'canary', 'v1', 'TRAVEL-001',
            'pre_submit', :source_fingerprint, true, false, 'completed',
            :idempotency_key, :payload_fingerprint
        )
        """
    )
    label_insert = text(
        """
        INSERT INTO agent_asset_release_labels (
            id, tenant_id, observation_id, asset_id, release_id, stage, version,
            label, verification_source, source_event_fingerprint,
            actor_fingerprint, idempotency_key, payload_fingerprint
        ) VALUES (
            :id, :tenant_id, 'observation-a', 'asset-a', 'release-a', 'canary',
            'v1', 'confirmed', 'typed_risk_disposition', :event_fingerprint,
            :actor_fingerprint, :idempotency_key, :payload_fingerprint
        )
        """
    )
    audit_sample_insert = text(
        """
        INSERT INTO agent_asset_release_audit_samples (
            id, tenant_id, observation_id, asset_id, release_id, stage, version,
            stratum, sampling_probability_ppm, selection_score_ppm,
            source_reference_encrypted, idempotency_key, payload_fingerprint
        ) VALUES (
            :id, :tenant_id, 'observation-a', 'asset-a', 'release-a', 'canary',
            'v1', :stratum, :sampling_probability_ppm, :selection_score_ppm,
            :source_reference_encrypted, :idempotency_key, :payload_fingerprint
        )
        """
    )

    with engine.connect() as connection:
        transaction = connection.begin()
        try:
            observation_parameters = {
                "id": "observation-a",
                "tenant_id": "tenant-a",
                "source_fingerprint": "a" * 64,
                "idempotency_key": "observation-request-a",
                "payload_fingerprint": "b" * 64,
            }
            connection.execute(observation_insert, observation_parameters)
            rejected(
                connection,
                observation_insert,
                {
                    **observation_parameters,
                    "id": "observation-duplicate",
                    "payload_fingerprint": "c" * 64,
                },
            )
            audit_sample_parameters = {
                "id": "audit-sample-a",
                "tenant_id": "tenant-a",
                "stratum": "candidate_negative_random",
                "sampling_probability_ppm": 200_000,
                "selection_score_ppm": 12_345,
                "source_reference_encrypted": "encrypted:opaque",
                "idempotency_key": "audit-sample-request-a",
                "payload_fingerprint": "1" * 64,
            }
            connection.execute(audit_sample_insert, audit_sample_parameters)
            rejected(
                connection,
                audit_sample_insert,
                {
                    **audit_sample_parameters,
                    "id": "audit-sample-duplicate-observation",
                    "idempotency_key": "audit-sample-request-duplicate",
                    "payload_fingerprint": "2" * 64,
                },
            )
            rejected(
                connection,
                audit_sample_insert,
                {
                    **audit_sample_parameters,
                    "id": "audit-sample-cross-tenant",
                    "tenant_id": "tenant-b",
                    "idempotency_key": "audit-sample-request-cross-tenant",
                },
            )
            label_parameters = {
                "id": "label-a",
                "tenant_id": "tenant-a",
                "event_fingerprint": "d" * 64,
                "actor_fingerprint": "e" * 64,
                "idempotency_key": "label-request-a",
                "payload_fingerprint": "f" * 64,
            }
            connection.execute(label_insert, label_parameters)
            rejected(
                connection,
                label_insert,
                {
                    **label_parameters,
                    "id": "label-cross-tenant",
                    "tenant_id": "tenant-b",
                    "idempotency_key": "label-request-cross-tenant",
                },
            )
            rejected(
                connection,
                text(
                    """
                    INSERT INTO agent_asset_release_labels (
                        id, tenant_id, observation_id, asset_id, release_id, stage,
                        version, label, verification_source, source_event_fingerprint,
                        actor_fingerprint, idempotency_key, payload_fingerprint
                    ) VALUES (
                        'label-invalid-semantics', 'tenant-a', 'observation-a',
                        'asset-a', 'release-a', 'canary', 'v1', 'risk_present',
                        'release_review', :event_fingerprint, :actor_fingerprint,
                        'label-invalid-semantics', :payload_fingerprint
                    )
                    """
                ),
                {
                    "event_fingerprint": "3" * 64,
                    "actor_fingerprint": "4" * 64,
                    "payload_fingerprint": "5" * 64,
                },
            )
            connection.execute(
                text(
                    """
                    INSERT INTO agent_asset_release_labels (
                        id, tenant_id, observation_id, asset_id, release_id, stage,
                        version, label, verification_source, source_event_fingerprint,
                        actor_fingerprint, idempotency_key, payload_fingerprint
                    ) VALUES (
                        'label-blind-a', 'tenant-a', 'observation-a', 'asset-a',
                        'release-a', 'canary', 'v1', 'risk_present',
                        'blind_release_review', :event_fingerprint, :actor_fingerprint,
                        'label-blind-a', :payload_fingerprint
                    )
                    """
                ),
                {
                    "event_fingerprint": "6" * 64,
                    "actor_fingerprint": "7" * 64,
                    "payload_fingerprint": "8" * 64,
                },
            )
            rejected(
                connection,
                text(
                    "UPDATE agent_asset_release_observations "
                    "SET failure_code = 'tampered' WHERE id = 'observation-a'"
                ),
                {},
                DBAPIError,
            )
            rejected(
                connection,
                text("DELETE FROM agent_asset_release_labels WHERE id = 'label-a'"),
                {},
                DBAPIError,
            )
            rejected(
                connection,
                text(
                    "UPDATE agent_asset_release_audit_samples "
                    "SET selection_score_ppm = 999999 WHERE id = 'audit-sample-a'"
                ),
                {},
                DBAPIError,
            )
        finally:
            transaction.rollback()
