mod agent;
mod api_key;
mod auth;
mod observability;
mod operation;
mod secret;
mod stream;
mod workspace;
mod yaml_import;

use crank_core::{
    AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AsyncJobHandle, AsyncJobId,
    AuthProfile, GraphqlOperationType, HttpMethod, InvitationId, InvitationToken, InvocationLog,
    InvocationLogId, JobStatus, MembershipRole, OperationId, OperationStatus, PlatformApiKey,
    PlatformApiKeyId, PlatformApiKeyStatus, Secret, SecretId, SecretVersion, StreamSession,
    StreamSessionId, StreamStatus, Target, UsageRollup, User, UserId, UserSessionId, Workspace,
    WorkspaceId,
};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;
use sqlx::{
    PgPool, Postgres, Row, Transaction,
    postgres::{PgConnectOptions, PgPoolOptions, PgRow},
    types::Json,
};
use std::{collections::BTreeMap, env, time::Duration};
use thiserror::Error;
use time::OffsetDateTime;

use crate::{
    error::RegistryError,
    migrations,
    model::{
        AgentSummary, AgentVersionRecord, AsyncJobFilter, AuthUserRecord, CreateAgentRequest,
        CreateAsyncJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
        CreatePlatformApiKeyRequest, CreateSecretRequest, CreateStreamSessionRequest,
        CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
        DescriptorMetadata, InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery,
        MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
        OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord,
        PublishAgentRequest, PublishRequest, PublishedAgentTool, RegistryOperation,
        RotateSecretRequest, SaveAgentBindingsRequest, SaveAuthProfileRequest,
        SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SecretRecord,
        SecretVersionRecord, SessionRecord, StreamSessionFilter, UpdateAsyncJobStatusRequest,
        UpdateStreamSessionStateRequest, UpdateWorkspaceRequest, UsageAgentBreakdown,
        UsageOperationBreakdown, UsageQuery, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
        WorkspaceMembershipRecord, WorkspaceRecord, YamlImportJob, YamlImportJobCompletion,
        YamlImportJobId, YamlImportJobStatus,
    },
};

#[derive(Clone, Debug)]
pub struct PostgresRegistry {
    pool: PgPool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PostgresPoolConfig {
    pub max_connections: u32,
    pub min_connections: u32,
    pub acquire_timeout_ms: u64,
    pub idle_timeout_ms: u64,
    pub max_lifetime_ms: u64,
}

#[derive(Debug, Error, PartialEq, Eq)]
pub enum PostgresPoolConfigError {
    #[error("invalid postgres pool setting {name}={value}")]
    InvalidValue { name: &'static str, value: String },
    #[error("POSTGRES_MAX_CONNECTIONS must be greater than zero")]
    ZeroMaxConnections,
    #[error("POSTGRES_MIN_CONNECTIONS must not exceed POSTGRES_MAX_CONNECTIONS")]
    MinConnectionsExceedMax,
}

impl Default for PostgresPoolConfig {
    fn default() -> Self {
        Self {
            max_connections: 20,
            min_connections: 2,
            acquire_timeout_ms: 5_000,
            idle_timeout_ms: 600_000,
            max_lifetime_ms: 1_800_000,
        }
    }
}

impl PostgresPoolConfig {
    pub fn from_env() -> Result<Self, PostgresPoolConfigError> {
        Self::from_vars(env::vars())
    }

    fn from_vars<I, K, V>(vars: I) -> Result<Self, PostgresPoolConfigError>
    where
        I: IntoIterator<Item = (K, V)>,
        K: AsRef<str>,
        V: AsRef<str>,
    {
        let vars = vars
            .into_iter()
            .map(|(name, value)| (name.as_ref().to_owned(), value.as_ref().to_owned()))
            .collect::<BTreeMap<_, _>>();
        let defaults = Self::default();
        let config = Self {
            max_connections: parse_u32_setting(
                &vars,
                "POSTGRES_MAX_CONNECTIONS",
                defaults.max_connections,
            )?,
            min_connections: parse_u32_setting(
                &vars,
                "POSTGRES_MIN_CONNECTIONS",
                defaults.min_connections,
            )?,
            acquire_timeout_ms: parse_u64_setting(
                &vars,
                "POSTGRES_ACQUIRE_TIMEOUT_MS",
                defaults.acquire_timeout_ms,
            )?,
            idle_timeout_ms: parse_u64_setting(
                &vars,
                "POSTGRES_IDLE_TIMEOUT_MS",
                defaults.idle_timeout_ms,
            )?,
            max_lifetime_ms: parse_u64_setting(
                &vars,
                "POSTGRES_MAX_LIFETIME_MS",
                defaults.max_lifetime_ms,
            )?,
        };

        config.validate()?;
        Ok(config)
    }

    fn validate(self) -> Result<Self, PostgresPoolConfigError> {
        if self.max_connections == 0 {
            return Err(PostgresPoolConfigError::ZeroMaxConnections);
        }
        if self.min_connections > self.max_connections {
            return Err(PostgresPoolConfigError::MinConnectionsExceedMax);
        }

        Ok(self)
    }
}

impl PostgresRegistry {
    pub async fn connect(database_url: &str) -> Result<Self, RegistryError> {
        Self::connect_in_schema(database_url, None).await
    }

    pub async fn connect_with_options(
        connect_options: PgConnectOptions,
    ) -> Result<Self, RegistryError> {
        Self::connect_with_options_and_pool_config(connect_options, PostgresPoolConfig::default())
            .await
    }

    pub async fn connect_with_options_and_pool_config(
        connect_options: PgConnectOptions,
        pool_config: PostgresPoolConfig,
    ) -> Result<Self, RegistryError> {
        let pool = PgPoolOptions::new()
            .max_connections(pool_config.max_connections)
            .min_connections(pool_config.min_connections)
            .acquire_timeout(Duration::from_millis(pool_config.acquire_timeout_ms))
            .idle_timeout(Duration::from_millis(pool_config.idle_timeout_ms))
            .max_lifetime(Duration::from_millis(pool_config.max_lifetime_ms))
            .connect_with(connect_options)
            .await?;
        migrations::apply_postgres(&pool).await?;
        Ok(Self { pool })
    }

    pub fn pool(&self) -> &PgPool {
        &self.pool
    }

    pub async fn migrate(&self) -> Result<(), RegistryError> {
        migrations::apply_postgres(&self.pool).await?;
        Ok(())
    }

    async fn connect_in_schema(
        database_url: &str,
        schema: Option<&str>,
    ) -> Result<Self, RegistryError> {
        let pool = PgPoolOptions::new()
            .min_connections(0)
            .max_connections(1)
            .idle_timeout(std::time::Duration::from_secs(1))
            .connect(database_url)
            .await?;

        if let Some(schema) = schema {
            sqlx::query(&format!("set search_path to {schema}"))
                .execute(&pool)
                .await?;
        }

        let registry = Self { pool };
        registry.migrate().await?;
        Ok(registry)
    }

    async fn list_agent_bindings(
        &self,
        agent_id: &AgentId,
        version: u32,
    ) -> Result<Vec<AgentOperationBinding>, RegistryError> {
        let rows = sqlx::query(
            "select
                operation_id,
                operation_version,
                tool_name,
                tool_title,
                tool_description_override,
                enabled
             from agent_operation_bindings
             where agent_id = $1 and agent_version = $2
             order by tool_name asc",
        )
        .bind(agent_id.as_str())
        .bind(to_db_version(version))
        .fetch_all(&self.pool)
        .await?;

        rows.iter().map(map_agent_binding).collect()
    }
}

fn parse_u32_setting(
    vars: &BTreeMap<String, String>,
    name: &'static str,
    default: u32,
) -> Result<u32, PostgresPoolConfigError> {
    vars.get(name)
        .map(|value| {
            value
                .parse::<u32>()
                .map_err(|_| PostgresPoolConfigError::InvalidValue {
                    name,
                    value: value.clone(),
                })
        })
        .transpose()
        .map(|value| value.unwrap_or(default))
}

fn parse_u64_setting(
    vars: &BTreeMap<String, String>,
    name: &'static str,
    default: u64,
) -> Result<u64, PostgresPoolConfigError> {
    vars.get(name)
        .map(|value| {
            value
                .parse::<u64>()
                .map_err(|_| PostgresPoolConfigError::InvalidValue {
                    name,
                    value: value.clone(),
                })
        })
        .transpose()
        .map(|value| value.unwrap_or(default))
}

#[cfg(test)]
mod pool_config_tests {
    use super::{PostgresPoolConfig, PostgresPoolConfigError};

    #[test]
    fn pool_config_uses_explicit_defaults() {
        let config = PostgresPoolConfig::from_vars(std::iter::empty::<(&str, &str)>()).unwrap();

        assert_eq!(
            config,
            PostgresPoolConfig {
                max_connections: 20,
                min_connections: 2,
                acquire_timeout_ms: 5_000,
                idle_timeout_ms: 600_000,
                max_lifetime_ms: 1_800_000,
            }
        );
    }

    #[test]
    fn pool_config_parses_overrides() {
        let config = PostgresPoolConfig::from_vars([
            ("POSTGRES_MAX_CONNECTIONS", "32"),
            ("POSTGRES_MIN_CONNECTIONS", "4"),
            ("POSTGRES_ACQUIRE_TIMEOUT_MS", "7000"),
            ("POSTGRES_IDLE_TIMEOUT_MS", "900000"),
            ("POSTGRES_MAX_LIFETIME_MS", "3600000"),
        ])
        .unwrap();

        assert_eq!(
            config,
            PostgresPoolConfig {
                max_connections: 32,
                min_connections: 4,
                acquire_timeout_ms: 7_000,
                idle_timeout_ms: 900_000,
                max_lifetime_ms: 3_600_000,
            }
        );
    }

    #[test]
    fn pool_config_rejects_invalid_numeric_value() {
        let error =
            PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "abc")]).unwrap_err();

        assert_eq!(
            error,
            PostgresPoolConfigError::InvalidValue {
                name: "POSTGRES_MAX_CONNECTIONS",
                value: "abc".to_owned(),
            }
        );
    }

    #[test]
    fn pool_config_rejects_zero_max_connections() {
        let error = PostgresPoolConfig::from_vars([("POSTGRES_MAX_CONNECTIONS", "0")]).unwrap_err();

        assert_eq!(error, PostgresPoolConfigError::ZeroMaxConnections);
    }

    #[test]
    fn pool_config_rejects_min_connections_above_max() {
        let error = PostgresPoolConfig::from_vars([
            ("POSTGRES_MAX_CONNECTIONS", "2"),
            ("POSTGRES_MIN_CONNECTIONS", "3"),
        ])
        .unwrap_err();

        assert_eq!(error, PostgresPoolConfigError::MinConnectionsExceedMax);
    }
}

async fn insert_version_row(
    tx: &mut Transaction<'_, Postgres>,
    snapshot: &RegistryOperation,
    change_note: Option<&str>,
    created_by: Option<&str>,
) -> Result<(), RegistryError> {
    sqlx::query(
        "insert into operation_versions (
            operation_id,
            version,
            status,
            target_json,
            input_schema_json,
            output_schema_json,
            input_mapping_json,
            output_mapping_json,
            execution_config_json,
            tool_description_json,
            samples_json,
            generated_draft_json,
            config_export_json,
            change_note,
            created_at,
            created_by
         ) values (
            $1, $2, $3, $4, $5, $6, $7, $8,
            $9, $10, $11, $12, $13, $14, $15::timestamptz, $16
         )",
    )
    .bind(snapshot.id.as_str())
    .bind(to_db_version(snapshot.version))
    .bind(serialize_enum_text(&snapshot.status, "status")?)
    .bind(Json(serialize_json_value(&snapshot.target)?))
    .bind(Json(serialize_json_value(&snapshot.input_schema)?))
    .bind(Json(serialize_json_value(&snapshot.output_schema)?))
    .bind(Json(serialize_json_value(&snapshot.input_mapping)?))
    .bind(Json(serialize_json_value(&snapshot.output_mapping)?))
    .bind(Json(serialize_json_value(&snapshot.execution_config)?))
    .bind(Json(serialize_json_value(&snapshot.tool_description)?))
    .bind(serialize_option_json_value(&snapshot.samples)?.map(Json))
    .bind(serialize_option_json_value(&snapshot.generated_draft)?.map(Json))
    .bind(serialize_option_json_value(&snapshot.config_export)?.map(Json))
    .bind(change_note)
    .bind(snapshot.updated_at)
    .bind(created_by)
    .execute(&mut **tx)
    .await?;

    Ok(())
}

async fn insert_agent_version_row(
    tx: &mut Transaction<'_, Postgres>,
    version: &AgentVersion,
) -> Result<(), RegistryError> {
    sqlx::query(
        "insert into agent_versions (
            agent_id,
            version,
            status,
            instructions_json,
            tool_selection_policy_json,
            created_at
         ) values (
            $1, $2, $3, $4, $5, $6::timestamptz
         )",
    )
    .bind(version.agent_id.as_str())
    .bind(to_db_version(version.version))
    .bind(serialize_enum_text(&version.status, "status")?)
    .bind(Json(version.instructions.clone()))
    .bind(Json(version.tool_selection_policy.clone()))
    .bind(version.created_at)
    .execute(&mut **tx)
    .await?;

    Ok(())
}

async fn replace_agent_bindings_rows(
    tx: &mut Transaction<'_, Postgres>,
    agent_id: &AgentId,
    version: u32,
    bindings: &[AgentOperationBinding],
) -> Result<(), RegistryError> {
    sqlx::query(
        "delete from agent_operation_bindings
         where agent_id = $1 and agent_version = $2",
    )
    .bind(agent_id.as_str())
    .bind(to_db_version(version))
    .execute(&mut **tx)
    .await?;

    for binding in bindings {
        sqlx::query(
            "insert into agent_operation_bindings (
                agent_id,
                agent_version,
                operation_id,
                operation_version,
                tool_name,
                tool_title,
                tool_description_override,
                enabled
             ) values ($1, $2, $3, $4, $5, $6, $7, $8)",
        )
        .bind(agent_id.as_str())
        .bind(to_db_version(version))
        .bind(binding.operation_id.as_str())
        .bind(to_db_version(binding.operation_version))
        .bind(&binding.tool_name)
        .bind(&binding.tool_title)
        .bind(binding.tool_description_override.as_deref())
        .bind(binding.enabled)
        .execute(&mut **tx)
        .await?;
    }

    Ok(())
}

fn assert_immutable_fields(
    summary: &OperationSummary,
    snapshot: &RegistryOperation,
) -> Result<(), RegistryError> {
    if summary.name != snapshot.name {
        return Err(RegistryError::ImmutableOperationFieldChanged {
            operation_id: snapshot.id.as_str().to_owned(),
            field: "name",
        });
    }

    if summary.protocol != snapshot.protocol {
        return Err(RegistryError::ImmutableOperationFieldChanged {
            operation_id: snapshot.id.as_str().to_owned(),
            field: "protocol",
        });
    }

    Ok(())
}

fn build_user(
    id: String,
    email: String,
    display_name: String,
    status: String,
    created_at: OffsetDateTime,
) -> Result<User, RegistryError> {
    Ok(User {
        id: UserId::new(id),
        email,
        display_name,
        status: deserialize_enum_text(&status, "status")?,
        created_at,
    })
}

fn map_user_update_error(error: sqlx::Error, user_id: &UserId, email: &str) -> RegistryError {
    match error {
        sqlx::Error::RowNotFound => RegistryError::UserNotFound {
            user_id: user_id.as_str().to_owned(),
        },
        sqlx::Error::Database(database_error)
            if database_error.code().as_deref() == Some("23505") =>
        {
            RegistryError::UserEmailAlreadyExists {
                email: email.to_owned(),
            }
        }
        other => RegistryError::Storage(other),
    }
}

fn map_stream_session(row: &PgRow) -> Result<StreamSession, RegistryError> {
    Ok(StreamSession {
        id: StreamSessionId::new(row.try_get::<String, _>("id")?),
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        agent_id: row
            .try_get::<Option<String>, _>("agent_id")?
            .map(AgentId::new),
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        protocol: deserialize_enum_text(&row.try_get::<String, _>("protocol")?, "protocol")?,
        mode: deserialize_enum_text(&row.try_get::<String, _>("mode")?, "mode")?,
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        cursor: row
            .try_get::<Option<Json<Value>>, _>("cursor_json")?
            .map(|value| value.0),
        state: row.try_get::<Json<Value>, _>("state_json")?.0,
        expires_at: row.try_get("expires_at")?,
        last_poll_at: row.try_get("last_poll_at")?,
        created_at: row.try_get("created_at")?,
        closed_at: row.try_get("closed_at")?,
    })
}

fn map_async_job(row: &PgRow) -> Result<AsyncJobHandle, RegistryError> {
    Ok(AsyncJobHandle {
        id: AsyncJobId::new(row.try_get::<String, _>("id")?),
        workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
        agent_id: row
            .try_get::<Option<String>, _>("agent_id")?
            .map(AgentId::new),
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        progress: row.try_get::<Json<Value>, _>("progress_json")?.0,
        result: row
            .try_get::<Option<Json<Value>>, _>("result_json")?
            .map(|value| value.0),
        error: row
            .try_get::<Option<Json<Value>>, _>("error_json")?
            .map(|value| value.0),
        expires_at: row.try_get("expires_at")?,
        last_poll_at: row.try_get("last_poll_at")?,
        created_at: row.try_get("created_at")?,
        updated_at: row.try_get("updated_at")?,
        finished_at: row.try_get("finished_at")?,
    })
}

fn map_invocation_log_record(row: &PgRow) -> Result<InvocationLogRecord, RegistryError> {
    Ok(InvocationLogRecord {
        log: InvocationLog {
            id: InvocationLogId::new(row.try_get::<String, _>("id")?),
            workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
            agent_id: row
                .try_get::<Option<String>, _>("agent_id")?
                .map(AgentId::new),
            operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
            source: deserialize_enum_text(&row.try_get::<String, _>("source")?, "source")?,
            level: deserialize_enum_text(&row.try_get::<String, _>("level")?, "level")?,
            status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
            tool_name: row.try_get("tool_name")?,
            message: row.try_get("message")?,
            request_id: row.try_get("request_id")?,
            status_code: match row.try_get::<Option<i32>, _>("status_code")? {
                Some(value) => {
                    Some(
                        u16::try_from(value).map_err(|_| RegistryError::InvalidNumericValue {
                            field: "status_code",
                            value: i64::from(value),
                        })?,
                    )
                }
                None => None,
            },
            duration_ms: to_u64(row.try_get::<i64, _>("duration_ms")?, "duration_ms")?,
            error_kind: row.try_get("error_kind")?,
            request_preview: row.try_get::<Json<Value>, _>("request_preview_json")?.0,
            response_preview: row.try_get::<Json<Value>, _>("response_preview_json")?.0,
            created_at: row.try_get("created_at")?,
        },
        operation_name: row.try_get("operation_name")?,
        operation_display_name: row.try_get("operation_display_name")?,
        agent_slug: row.try_get("agent_slug")?,
        agent_display_name: row.try_get("agent_display_name")?,
    })
}

fn map_operation_usage_summary(row: &PgRow) -> Result<OperationUsageSummary, RegistryError> {
    build_operation_usage_summary(
        row.try_get("operation_id")?,
        row.try_get("calls_today")?,
        row.try_get("error_rate_pct")?,
        row.try_get("avg_latency_ms")?,
    )
}

fn target_summary(target: &Target) -> (String, String) {
    match target {
        Target::Rest(rest) => (
            join_target_url(&rest.base_url, &rest.path_template),
            match rest.method {
                HttpMethod::Get => "GET",
                HttpMethod::Post => "POST",
                HttpMethod::Put => "PUT",
                HttpMethod::Patch => "PATCH",
                HttpMethod::Delete => "DELETE",
            }
            .to_owned(),
        ),
        Target::Graphql(graphql) => (
            graphql.endpoint.clone(),
            match graphql.operation_type {
                GraphqlOperationType::Query => "QUERY",
                GraphqlOperationType::Mutation => "MUTATION",
            }
            .to_owned(),
        ),
        Target::Grpc(grpc) => (grpc.server_addr.clone(), grpc.method.clone()),
        Target::Websocket(websocket) => (websocket.url.clone(), "SUBSCRIBE".to_owned()),
        Target::Soap(soap) => (
            soap.endpoint_override
                .clone()
                .unwrap_or_else(|| format!("wsdl://{}", soap.service_name)),
            soap.operation_name.clone(),
        ),
    }
}

fn join_target_url(base: &str, path: &str) -> String {
    if path.is_empty() {
        return base.to_owned();
    }

    if path.starts_with("http://") || path.starts_with("https://") || path.starts_with("grpc://") {
        return path.to_owned();
    }

    let trimmed_base = base.trim_end_matches('/');
    let trimmed_path = path.trim_start_matches('/');

    if trimmed_base.is_empty() {
        format!("/{trimmed_path}")
    } else if trimmed_path.is_empty() {
        trimmed_base.to_owned()
    } else {
        format!("{trimmed_base}/{trimmed_path}")
    }
}

fn map_agent_binding(row: &PgRow) -> Result<AgentOperationBinding, RegistryError> {
    Ok(AgentOperationBinding {
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        operation_version: from_db_version(row.try_get("operation_version")?, "operation_version")?,
        tool_name: row.try_get("tool_name")?,
        tool_title: row.try_get("tool_title")?,
        tool_description_override: row.try_get("tool_description_override")?,
        enabled: row.try_get("enabled")?,
    })
}

#[allow(clippy::too_many_arguments)]
fn build_agent_summary(
    id: String,
    workspace_id: String,
    slug: String,
    display_name: String,
    description: String,
    status: String,
    current_draft_version: i32,
    latest_published_version: Option<i32>,
    created_at: OffsetDateTime,
    updated_at: OffsetDateTime,
    published_at: Option<OffsetDateTime>,
) -> Result<AgentSummary, RegistryError> {
    Ok(AgentSummary {
        id: AgentId::new(id),
        workspace_id: WorkspaceId::new(workspace_id),
        slug,
        display_name,
        description,
        status: deserialize_enum_text(&status, "status")?,
        current_draft_version: from_db_version(current_draft_version, "current_draft_version")?,
        latest_published_version: latest_published_version
            .map(|value| from_db_version(value, "latest_published_version"))
            .transpose()?,
        created_at,
        updated_at,
        published_at,
    })
}

#[allow(clippy::too_many_arguments)]
fn build_operation_summary(
    id: String,
    workspace_id: String,
    name: String,
    display_name: String,
    category: String,
    protocol: String,
    security_level: String,
    target_json: Value,
    status: String,
    current_draft_version: i32,
    latest_published_version: Option<i32>,
    created_at: OffsetDateTime,
    updated_at: OffsetDateTime,
    published_at: Option<OffsetDateTime>,
) -> Result<OperationSummary, RegistryError> {
    let target: Target = deserialize_json_value(target_json)?;
    let (target_url, target_action) = target_summary(&target);

    Ok(OperationSummary {
        id: OperationId::new(id),
        workspace_id: WorkspaceId::new(workspace_id),
        name,
        display_name,
        category,
        protocol: deserialize_enum_text(&protocol, "protocol")?,
        security_level: deserialize_enum_text(&security_level, "security_level")?,
        target_url,
        target_action,
        status: deserialize_enum_text(&status, "status")?,
        current_draft_version: from_db_version(current_draft_version, "current_draft_version")?,
        latest_published_version: latest_published_version
            .map(|value| from_db_version(value, "latest_published_version"))
            .transpose()?,
        created_at,
        updated_at,
        published_at,
    })
}

fn build_auth_profile(
    id: String,
    workspace_id: String,
    name: String,
    kind: String,
    config_json: Value,
    created_at: OffsetDateTime,
    updated_at: OffsetDateTime,
) -> Result<AuthProfile, RegistryError> {
    Ok(AuthProfile {
        id: crank_core::AuthProfileId::new(id),
        workspace_id: WorkspaceId::new(workspace_id),
        name,
        kind: deserialize_enum_text(&kind, "kind")?,
        config: deserialize_json_value(config_json)?,
        created_at,
        updated_at,
    })
}

fn build_operation_usage_summary(
    operation_id: String,
    calls_today: i64,
    error_rate_pct: f64,
    avg_latency_ms: i64,
) -> Result<OperationUsageSummary, RegistryError> {
    Ok(OperationUsageSummary {
        operation_id: OperationId::new(operation_id),
        calls_today: to_u64(calls_today, "calls_today")?,
        error_rate_pct,
        avg_latency_ms: to_u64(avg_latency_ms, "avg_latency_ms")?,
    })
}

fn build_operation_agent_ref(
    operation_id: String,
    agent_id: String,
    agent_slug: String,
    display_name: String,
) -> Result<OperationAgentRef, RegistryError> {
    Ok(OperationAgentRef {
        operation_id: OperationId::new(operation_id),
        agent_id: AgentId::new(agent_id),
        agent_slug,
        display_name,
    })
}

fn build_agent_version_record(
    summary: &AgentSummary,
    version: i32,
    status: String,
    instructions_json: Value,
    tool_selection_policy_json: Value,
    created_at: OffsetDateTime,
    bindings: Vec<AgentOperationBinding>,
) -> Result<AgentVersionRecord, RegistryError> {
    let version = from_db_version(version, "version")?;
    let status = deserialize_enum_text(&status, "status")?;

    Ok(AgentVersionRecord {
        agent_id: summary.id.clone(),
        workspace_id: summary.workspace_id.clone(),
        version,
        status,
        created_at,
        bindings,
        snapshot: AgentVersion {
            agent_id: summary.id.clone(),
            version,
            status,
            instructions: instructions_json,
            tool_selection_policy: tool_selection_policy_json,
            created_at,
        },
    })
}

#[allow(clippy::too_many_arguments)]
fn build_operation_version_record(
    id: String,
    workspace_id: String,
    name: String,
    display_name: String,
    category: String,
    protocol: String,
    security_level: String,
    operation_created_at: OffsetDateTime,
    operation_updated_at: OffsetDateTime,
    operation_published_at: Option<OffsetDateTime>,
    version: i32,
    status: String,
    target_json: Value,
    input_schema_json: Value,
    output_schema_json: Value,
    input_mapping_json: Value,
    output_mapping_json: Value,
    execution_config_json: Value,
    tool_description_json: Value,
    samples_json: Option<Value>,
    generated_draft_json: Option<Value>,
    config_export_json: Option<Value>,
    change_note: Option<String>,
    created_at: OffsetDateTime,
    created_by: Option<String>,
) -> Result<OperationVersionRecord, RegistryError> {
    let operation_id = OperationId::new(id);
    let version = from_db_version(version, "version")?;
    let status = deserialize_enum_text(&status, "status")?;

    Ok(OperationVersionRecord {
        operation_id: operation_id.clone(),
        workspace_id: WorkspaceId::new(workspace_id),
        version,
        status,
        change_note,
        created_at,
        created_by,
        snapshot: RegistryOperation {
            id: operation_id,
            name,
            display_name,
            category,
            protocol: deserialize_enum_text(&protocol, "protocol")?,
            security_level: deserialize_enum_text(&security_level, "security_level")?,
            status,
            version,
            target: deserialize_json_value(target_json)?,
            input_schema: deserialize_json_value(input_schema_json)?,
            output_schema: deserialize_json_value(output_schema_json)?,
            input_mapping: deserialize_json_value(input_mapping_json)?,
            output_mapping: deserialize_json_value(output_mapping_json)?,
            execution_config: deserialize_json_value(execution_config_json)?,
            tool_description: deserialize_json_value(tool_description_json)?,
            samples: samples_json.map(deserialize_json_value).transpose()?,
            generated_draft: generated_draft_json
                .map(deserialize_json_value)
                .transpose()?,
            config_export: config_export_json.map(deserialize_json_value).transpose()?,
            created_at: operation_created_at,
            updated_at: operation_updated_at,
            published_at: operation_published_at,
        },
    })
}

#[allow(clippy::too_many_arguments)]
fn build_published_agent_tool(
    workspace_id: String,
    workspace_slug: String,
    agent_id: String,
    agent_slug: String,
    tool_name: String,
    tool_title: String,
    tool_description: String,
    operation: RegistryOperation,
) -> Result<PublishedAgentTool, RegistryError> {
    Ok(PublishedAgentTool {
        workspace_id: WorkspaceId::new(workspace_id),
        workspace_slug,
        agent_id: AgentId::new(agent_id),
        agent_slug,
        tool_name,
        tool_title,
        tool_description,
        operation,
    })
}

#[allow(clippy::too_many_arguments)]
fn build_sample_metadata(
    id: String,
    operation_id: String,
    version: i32,
    sample_kind: String,
    storage_ref: String,
    content_type: String,
    file_name: Option<String>,
    created_at: OffsetDateTime,
) -> Result<OperationSampleMetadata, RegistryError> {
    Ok(OperationSampleMetadata {
        id: crank_core::SampleId::new(id),
        operation_id: OperationId::new(operation_id),
        version: from_db_version(version, "version")?,
        sample_kind: deserialize_enum_text(&sample_kind, "sample_kind")?,
        storage_ref,
        content_type,
        file_name,
        created_at,
    })
}

#[allow(clippy::too_many_arguments)]
fn build_descriptor_metadata(
    id: String,
    operation_id: Option<String>,
    version: Option<i32>,
    descriptor_kind: String,
    storage_ref: String,
    source_name: Option<String>,
    package_index: Option<Value>,
    created_at: OffsetDateTime,
) -> Result<DescriptorMetadata, RegistryError> {
    Ok(DescriptorMetadata {
        id: crank_core::DescriptorId::new(id),
        operation_id: operation_id.map(OperationId::new),
        version: version
            .map(|value| from_db_version(value, "version"))
            .transpose()?,
        descriptor_kind: deserialize_enum_text(&descriptor_kind, "descriptor_kind")?,
        storage_ref,
        source_name,
        package_index,
        created_at,
    })
}

fn map_yaml_import_job(row: &PgRow) -> Result<YamlImportJob, RegistryError> {
    Ok(YamlImportJob {
        id: YamlImportJobId::new(row.try_get::<String, _>("id")?),
        source_sample_id: row
            .try_get::<Option<String>, _>("source_sample_id")?
            .map(crank_core::SampleId::new),
        status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
        format_version: row.try_get("format_version")?,
        mode: deserialize_enum_text(&row.try_get::<String, _>("mode")?, "mode")?,
        result_operation_id: row
            .try_get::<Option<String>, _>("result_operation_id")?
            .map(OperationId::new),
        result_version: row
            .try_get::<Option<i32>, _>("result_version")?
            .map(|value| from_db_version(value, "result_version"))
            .transpose()?,
        error_text: row.try_get("error_text")?,
        created_at: row.try_get("created_at")?,
        finished_at: row.try_get("finished_at")?,
    })
}

fn map_usage_operation_breakdown(row: &PgRow) -> Result<UsageOperationBreakdown, RegistryError> {
    Ok(UsageOperationBreakdown {
        operation_id: OperationId::new(row.try_get::<String, _>("operation_id")?),
        operation_name: row.try_get("operation_name")?,
        operation_display_name: row.try_get("operation_display_name")?,
        protocol: deserialize_enum_text(&row.try_get::<String, _>("protocol")?, "protocol")?,
        calls_total: to_u64(row.try_get::<i64, _>("calls_total")?, "calls_total")?,
        calls_error: to_u64(row.try_get::<i64, _>("calls_error")?, "calls_error")?,
        p50_ms: to_u64(row.try_get::<i64, _>("p50_ms")?, "p50_ms")?,
        p95_ms: to_u64(row.try_get::<i64, _>("p95_ms")?, "p95_ms")?,
        p99_ms: to_u64(row.try_get::<i64, _>("p99_ms")?, "p99_ms")?,
    })
}

fn map_usage_agent_breakdown(row: &PgRow) -> Result<UsageAgentBreakdown, RegistryError> {
    Ok(UsageAgentBreakdown {
        agent_id: AgentId::new(row.try_get::<String, _>("agent_id")?),
        agent_slug: row.try_get("agent_slug")?,
        agent_display_name: row.try_get("agent_display_name")?,
        calls_total: to_u64(row.try_get::<i64, _>("calls_total")?, "calls_total")?,
        calls_error: to_u64(row.try_get::<i64, _>("calls_error")?, "calls_error")?,
        p50_ms: to_u64(row.try_get::<i64, _>("p50_ms")?, "p50_ms")?,
        p95_ms: to_u64(row.try_get::<i64, _>("p95_ms")?, "p95_ms")?,
        p99_ms: to_u64(row.try_get::<i64, _>("p99_ms")?, "p99_ms")?,
    })
}

fn serialize_json_value<T: Serialize>(value: &T) -> Result<Value, RegistryError> {
    Ok(serde_json::to_value(value)?)
}

fn serialize_option_json_value<T: Serialize>(
    value: &Option<T>,
) -> Result<Option<Value>, RegistryError> {
    value.as_ref().map(serialize_json_value).transpose()
}

fn deserialize_json_value<T: DeserializeOwned>(value: Value) -> Result<T, RegistryError> {
    Ok(serde_json::from_value(value)?)
}

fn serialize_enum_text<T: Serialize>(
    value: &T,
    field: &'static str,
) -> Result<String, RegistryError> {
    serde_json::to_value(value)?
        .as_str()
        .map(ToOwned::to_owned)
        .ok_or(RegistryError::InvalidEnumRepresentation { field })
}

fn deserialize_enum_text<T: DeserializeOwned>(
    value: &str,
    field: &'static str,
) -> Result<T, RegistryError> {
    serde_json::from_value(Value::String(value.to_owned()))
        .map_err(|_| RegistryError::InvalidEnumRepresentation { field })
}

fn to_db_version(value: u32) -> i32 {
    value as i32
}

fn from_db_version(value: i32, field: &'static str) -> Result<u32, RegistryError> {
    u32::try_from(value).map_err(|_| RegistryError::InvalidNumericValue {
        field,
        value: i64::from(value),
    })
}

fn validate_stream_session_transition(
    session_id: &StreamSessionId,
    from: StreamStatus,
    to: StreamStatus,
) -> Result<(), RegistryError> {
    let allowed = matches!(
        (from, to),
        (StreamStatus::Created, StreamStatus::Running)
            | (StreamStatus::Running, StreamStatus::Running)
            | (StreamStatus::Running, StreamStatus::Stopped)
            | (StreamStatus::Running, StreamStatus::Expired)
            | (StreamStatus::Running, StreamStatus::Failed)
    );

    if allowed {
        Ok(())
    } else {
        Err(RegistryError::InvalidStreamSessionTransition {
            session_id: session_id.as_str().to_owned(),
            from: serialize_enum_text(&from, "status").unwrap_or_else(|_| "unknown".to_owned()),
            to: serialize_enum_text(&to, "status").unwrap_or_else(|_| "unknown".to_owned()),
        })
    }
}

fn validate_async_job_transition(
    job_id: &AsyncJobId,
    from: JobStatus,
    to: JobStatus,
) -> Result<(), RegistryError> {
    let allowed = matches!(
        (from, to),
        (JobStatus::Created, JobStatus::Running)
            | (JobStatus::Running, JobStatus::Running)
            | (JobStatus::Running, JobStatus::Completed)
            | (JobStatus::Running, JobStatus::Failed)
            | (JobStatus::Running, JobStatus::Cancelled)
            | (JobStatus::Completed, JobStatus::Expired)
            | (JobStatus::Failed, JobStatus::Expired)
            | (JobStatus::Cancelled, JobStatus::Expired)
    );

    if allowed {
        Ok(())
    } else {
        Err(RegistryError::InvalidAsyncJobTransition {
            job_id: job_id.as_str().to_owned(),
            from: serialize_enum_text(&from, "status").unwrap_or_else(|_| "unknown".to_owned()),
            to: serialize_enum_text(&to, "status").unwrap_or_else(|_| "unknown".to_owned()),
        })
    }
}

fn to_u64(value: i64, field: &'static str) -> Result<u64, RegistryError> {
    u64::try_from(value).map_err(|_| RegistryError::InvalidNumericValue { field, value })
}

fn usage_bucket_sql(bucket: crate::model::UsageBucket) -> &'static str {
    match bucket {
        crate::model::UsageBucket::Hour => "hour",
        crate::model::UsageBucket::Day => "day",
        crate::model::UsageBucket::Week => "week",
        crate::model::UsageBucket::Month => "month",
    }
}

#[cfg(test)]
mod tests {
    use std::{
        collections::BTreeMap,
        env,
        time::{SystemTime, UNIX_EPOCH},
    };

    use crank_core::{
        AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig,
        AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, ConfigExport, ExecutionConfig,
        ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, InvocationLog, JobStatus,
        MembershipRole, OperationId, OperationSecurityLevel, OperationStatus, PlatformApiKey,
        PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget,
        RetryPolicy, Samples, SecretId, StreamSession, StreamSessionId, StreamStatus, Target,
        ToolDescription, ToolExample, User, UserId, UserSessionId, Workspace, WorkspaceId,
    };
    use crank_mapping::{MappingRule, MappingSet};
    use crank_schema::{Schema, SchemaKind};
    use serde_json::json;
    use sqlx::{Executor, PgPool, postgres::PgPoolOptions};
    use time::{OffsetDateTime, format_description::well_known::Rfc3339};

    use crate::{
        PostgresRegistry, RegistryError,
        model::{
            AsyncJobFilter, CreateAgentRequest, CreateAsyncJobRequest, CreateInvocationLogRequest,
            CreatePlatformApiKeyRequest, CreateStreamSessionRequest, CreateVersionRequest,
            CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata,
            OperationSampleMetadata, PlatformApiKeyRecord, PublishAgentRequest, PublishRequest,
            RegistryOperation, SampleKind, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
            SaveSampleMetadataRequest, StreamSessionFilter, UpdateAsyncJobStatusRequest,
            UpdateStreamSessionStateRequest, WorkspaceRecord, YamlImportJobCompletion,
            YamlImportJobId, YamlImportJobStatus,
        },
    };

    fn test_workspace_id() -> WorkspaceId {
        WorkspaceId::new("ws_default")
    }

    fn timestamp(value: &str) -> OffsetDateTime {
        OffsetDateTime::parse(value, &Rfc3339).unwrap()
    }

    #[tokio::test]
    async fn stores_versions_and_published_operations() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation_v1 = test_operation("op_rest_01", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation_v1, Some("alice"))
            .await
            .unwrap();

        let operation_v2 = test_operation("op_rest_01", 2, OperationStatus::Draft);

        registry
            .create_version(CreateVersionRequest {
                workspace_id: &test_workspace_id(),
                snapshot: &operation_v2,
                change_note: Some("add output mapping"),
                created_by: Some("alice"),
            })
            .await
            .unwrap();

        registry
            .publish_operation(PublishRequest {
                workspace_id: &test_workspace_id(),
                operation_id: &operation_v2.id,
                version: operation_v2.version,
                published_at: &timestamp("2026-03-25T12:10:00Z"),
                published_by: Some("alice"),
            })
            .await
            .unwrap();

        let summary = registry
            .get_operation_summary(&test_workspace_id(), &operation_v2.id)
            .await
            .unwrap()
            .unwrap();
        let versions = registry
            .list_operation_versions(&test_workspace_id(), &operation_v2.id)
            .await
            .unwrap();
        let published = registry
            .get_published_operation(&operation_v2.id)
            .await
            .unwrap()
            .unwrap();
        let published_list = registry.list_published_operations().await.unwrap();

        assert_eq!(summary.current_draft_version, 2);
        assert_eq!(summary.latest_published_version, Some(2));
        assert_eq!(summary.status, OperationStatus::Published);
        assert_eq!(versions.len(), 2);
        assert_eq!(
            versions[1].change_note.as_deref(),
            Some("add output mapping")
        );
        assert_eq!(published.version, 2);
        assert!(published.is_published());
        assert_eq!(published_list, vec![published.clone()]);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn rejects_out_of_order_versions() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_rest_02", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        let invalid = test_operation("op_rest_02", 3, OperationStatus::Draft);
        let error = registry
            .create_version(CreateVersionRequest {
                workspace_id: &test_workspace_id(),
                snapshot: &invalid,
                change_note: None,
                created_by: None,
            })
            .await
            .unwrap_err();

        assert!(matches!(
            error,
            RegistryError::InvalidVersionSequence {
                expected: 2,
                actual: 3,
                ..
            }
        ));

        database.cleanup().await;
    }

    #[tokio::test]
    async fn update_operation_draft_persists_optional_json_columns_as_sql_null() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let mut operation = test_operation("op_rest_02b", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        operation.generated_draft = None;
        operation.samples = None;
        operation.config_export = None;
        operation.updated_at = timestamp("2026-03-25T12:34:00Z");

        registry
            .update_operation_draft(&test_workspace_id(), &operation)
            .await
            .unwrap();

        let stored = registry
            .get_operation_version(&test_workspace_id(), &operation.id, operation.version)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(stored.snapshot.generated_draft, None);
        assert_eq!(stored.snapshot.samples, None);
        assert_eq!(stored.snapshot.config_export, None);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn stores_auth_profiles_and_artifact_metadata() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_rest_03", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        let auth_profile = AuthProfile {
            id: "auth_crank".into(),
            workspace_id: test_workspace_id(),
            name: "Crank API key".to_owned(),
            kind: AuthKind::ApiKeyHeader,
            config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig {
                header_name: "X-Api-Key".to_owned(),
                secret_id: SecretId::new("secret_crank_api_key"),
            }),
            created_at: timestamp("2026-03-25T12:00:00Z"),
            updated_at: timestamp("2026-03-25T12:00:00Z"),
        };

        registry
            .save_auth_profile(SaveAuthProfileRequest {
                workspace_id: &test_workspace_id(),
                profile: &auth_profile,
            })
            .await
            .unwrap();

        let input_sample = OperationSampleMetadata {
            id: "sample_input".into(),
            operation_id: operation.id.clone(),
            version: 1,
            sample_kind: SampleKind::InputJson,
            storage_ref: "file:///tmp/input.json".to_owned(),
            content_type: "application/json".to_owned(),
            file_name: Some("input.json".to_owned()),
            created_at: timestamp("2026-03-25T12:01:00Z"),
        };
        let descriptor = DescriptorMetadata {
            id: "descriptor_01".into(),
            operation_id: Some(operation.id.clone()),
            version: Some(1),
            descriptor_kind: DescriptorKind::DescriptorSet,
            storage_ref: "file:///tmp/schema.desc".to_owned(),
            source_name: Some("schema.desc".to_owned()),
            package_index: Some(json!({ "crm.v1": ["LeadService"] })),
            created_at: timestamp("2026-03-25T12:02:00Z"),
        };

        registry
            .save_sample_metadata(SaveSampleMetadataRequest {
                sample: &input_sample,
            })
            .await
            .unwrap();
        registry
            .save_descriptor_metadata(SaveDescriptorMetadataRequest {
                descriptor: &descriptor,
            })
            .await
            .unwrap();

        let auth_profiles = registry
            .list_auth_profiles(&test_workspace_id())
            .await
            .unwrap();
        let samples = registry
            .list_sample_metadata(&operation.id, 1)
            .await
            .unwrap();
        let descriptors = registry
            .list_descriptor_metadata(&operation.id, 1)
            .await
            .unwrap();

        assert_eq!(auth_profiles, vec![auth_profile]);
        assert_eq!(samples, vec![input_sample]);
        assert_eq!(descriptors, vec![descriptor]);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn lists_auth_profiles_referencing_secret() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let primary_secret_id = SecretId::new("secret_primary");
        let secondary_secret_id = SecretId::new("secret_secondary");
        let profile = AuthProfile {
            id: "auth_crank".into(),
            workspace_id: test_workspace_id(),
            name: "Crank basic auth".to_owned(),
            kind: AuthKind::Basic,
            config: AuthConfig::Basic(crank_core::BasicAuthConfig {
                username_secret_id: primary_secret_id.clone(),
                password_secret_id: secondary_secret_id.clone(),
            }),
            created_at: timestamp("2026-03-25T12:00:00Z"),
            updated_at: timestamp("2026-03-25T12:00:00Z"),
        };

        registry
            .save_auth_profile(SaveAuthProfileRequest {
                workspace_id: &test_workspace_id(),
                profile: &profile,
            })
            .await
            .unwrap();

        let profiles = registry
            .list_auth_profiles_referencing_secret(&test_workspace_id(), &primary_secret_id)
            .await
            .unwrap();

        assert_eq!(profiles, vec![profile]);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn stores_and_finishes_yaml_import_jobs() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let job_id = YamlImportJobId::new("job_yaml_01");
        let operation = test_operation("op_rest_04", 1, OperationStatus::Draft);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        registry
            .create_yaml_import_job(CreateYamlImportJobRequest {
                id: &job_id,
                source_sample_id: None,
                format_version: "v1",
                mode: ExportMode::Portable,
                created_at: &timestamp("2026-03-25T12:00:00Z"),
            })
            .await
            .unwrap();

        registry
            .finish_yaml_import_job(
                &job_id,
                &YamlImportJobCompletion {
                    status: YamlImportJobStatus::Completed,
                    result_operation_id: Some(operation.id.clone()),
                    result_version: Some(2),
                    error_text: None,
                    finished_at: timestamp("2026-03-25T12:05:00Z"),
                },
            )
            .await
            .unwrap();

        let job = registry
            .get_yaml_import_job(&job_id)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(job.status, YamlImportJobStatus::Completed);
        assert_eq!(job.result_version, Some(2));
        assert_eq!(job.mode, ExportMode::Portable);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_workspace_read_paths() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let workspace = Workspace {
            id: WorkspaceId::new("ws_extra_01"),
            slug: "extra".to_owned(),
            display_name: "Extra Workspace".to_owned(),
            status: crank_core::WorkspaceStatus::Active,
            settings: json!({"region":"eu"}),
            created_at: timestamp("2026-03-25T12:00:00Z"),
            updated_at: timestamp("2026-03-25T12:00:00Z"),
        };
        let mut user = User {
            id: UserId::new("user_extra_01"),
            email: "owner@example.com".to_owned(),
            display_name: "Owner".to_owned(),
            status: crank_core::UserStatus::Active,
            created_at: timestamp("2026-03-25T11:00:00Z"),
        };

        registry
            .create_workspace(CreateWorkspaceRequest {
                workspace: &workspace,
            })
            .await
            .unwrap();
        let user_id = registry
            .upsert_bootstrap_user(&user.email, &user.display_name, "hashed-password")
            .await
            .unwrap();
        user.id = user_id;
        registry
            .ensure_membership(&workspace.id, &user.id, MembershipRole::Owner)
            .await
            .unwrap();

        let all_workspaces = registry.list_workspaces().await.unwrap();
        let user_workspaces = registry.list_workspaces_for_user(&user.id).await.unwrap();
        let memberships = registry.list_memberships(&workspace.id).await.unwrap();
        let loaded = registry
            .get_workspace(&workspace.id)
            .await
            .unwrap()
            .unwrap();

        assert!(
            all_workspaces
                .iter()
                .any(|record| record.workspace == workspace)
        );
        assert_eq!(user_workspaces.len(), 1);
        assert_eq!(user_workspaces[0].workspace, workspace);
        assert_eq!(user_workspaces[0].role, MembershipRole::Owner);
        assert_eq!(memberships.len(), 1);
        assert_eq!(memberships[0].workspace_id, workspace.id);
        assert_eq!(memberships[0].user.id, user.id);
        assert_eq!(memberships[0].user.email, user.email);
        assert_eq!(memberships[0].user.display_name, user.display_name);
        assert_eq!(memberships[0].user.status, user.status);
        assert_eq!(memberships[0].role, MembershipRole::Owner);
        assert_eq!(loaded, WorkspaceRecord { workspace });

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_user_profile_and_workspace_access_reads() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let workspace = Workspace {
            id: WorkspaceId::new("ws_profile_01"),
            slug: "profile".to_owned(),
            display_name: "Profile Workspace".to_owned(),
            status: crank_core::WorkspaceStatus::Active,
            settings: json!({}),
            created_at: timestamp("2026-03-25T12:00:00Z"),
            updated_at: timestamp("2026-03-25T12:00:00Z"),
        };
        let other_workspace = Workspace {
            id: WorkspaceId::new("ws_profile_02"),
            slug: "profile-other".to_owned(),
            display_name: "Other Workspace".to_owned(),
            status: crank_core::WorkspaceStatus::Active,
            settings: json!({}),
            created_at: timestamp("2026-03-25T12:01:00Z"),
            updated_at: timestamp("2026-03-25T12:01:00Z"),
        };
        let user_id = registry
            .upsert_bootstrap_user("profile@example.com", "Owner", "hashed-password")
            .await
            .unwrap();

        registry
            .create_workspace(CreateWorkspaceRequest {
                workspace: &workspace,
            })
            .await
            .unwrap();
        registry
            .create_workspace(CreateWorkspaceRequest {
                workspace: &other_workspace,
            })
            .await
            .unwrap();
        registry
            .ensure_membership(&workspace.id, &user_id, MembershipRole::Owner)
            .await
            .unwrap();

        let updated = registry
            .update_user_profile(&user_id, "updated@example.com", "Updated Owner")
            .await
            .unwrap();
        let has_access = registry
            .user_has_workspace_access(&user_id, &workspace.id)
            .await
            .unwrap();
        let lacks_access = registry
            .user_has_workspace_access(&user_id, &other_workspace.id)
            .await
            .unwrap();

        assert_eq!(updated.id, user_id);
        assert_eq!(updated.email, "updated@example.com");
        assert_eq!(updated.display_name, "Updated Owner");
        assert!(has_access);
        assert!(!lacks_access);
        assert!(updated.created_at.unix_timestamp() > 0);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn creates_and_loads_user_sessions_with_typed_expiration() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let workspace = Workspace {
            id: WorkspaceId::new("ws_session_01"),
            slug: "session".to_owned(),
            display_name: "Session Workspace".to_owned(),
            status: crank_core::WorkspaceStatus::Active,
            settings: json!({}),
            created_at: timestamp("2026-03-25T12:00:00Z"),
            updated_at: timestamp("2026-03-25T12:00:00Z"),
        };
        let user_id = registry
            .upsert_bootstrap_user("session@example.com", "Owner", "hashed-password")
            .await
            .unwrap();
        let session_id = UserSessionId::new("sess_01");
        let expires_at = timestamp("2030-04-06T12:05:00Z");

        registry
            .create_workspace(CreateWorkspaceRequest {
                workspace: &workspace,
            })
            .await
            .unwrap();
        registry
            .ensure_membership(&workspace.id, &user_id, MembershipRole::Owner)
            .await
            .unwrap();
        registry
            .create_user_session(
                &session_id,
                &user_id,
                Some(&workspace.id),
                "secret-hash-01",
                &expires_at,
            )
            .await
            .unwrap();

        let session = registry
            .get_user_session(&session_id, "secret-hash-01")
            .await
            .unwrap()
            .unwrap();

        assert_eq!(session.session_id, session_id);
        assert_eq!(session.current_workspace_id, Some(workspace.id.clone()));
        assert_eq!(session.user.id, user_id);
        assert!(session.user.created_at.unix_timestamp() > 0);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_platform_api_key_read_paths() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let workspace = Workspace {
            id: WorkspaceId::new("ws_keys_01"),
            slug: "keys".to_owned(),
            display_name: "Keys Workspace".to_owned(),
            status: crank_core::WorkspaceStatus::Active,
            settings: json!({}),
            created_at: timestamp("2026-03-25T12:00:00Z"),
            updated_at: timestamp("2026-03-25T12:00:00Z"),
        };
        let agent = test_agent("agent_keys_01", AgentStatus::Draft);
        let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
        let api_key = PlatformApiKey {
            id: PlatformApiKeyId::new("key_01"),
            workspace_id: workspace.id.clone(),
            agent_id: Some(agent.id.clone()),
            name: "Primary".to_owned(),
            prefix: "crk_live".to_owned(),
            scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
            status: PlatformApiKeyStatus::Active,
            created_at: timestamp("2026-03-25T12:01:00Z"),
            last_used_at: None,
        };
        let secret_hash = "secret_hash_01";

        registry
            .create_workspace(CreateWorkspaceRequest {
                workspace: &workspace,
            })
            .await
            .unwrap();
        registry
            .create_agent(CreateAgentRequest {
                agent: &agent,
                version: &version,
                bindings: &[],
            })
            .await
            .unwrap();
        registry
            .create_platform_api_key(CreatePlatformApiKeyRequest {
                api_key: &api_key,
                secret_hash,
            })
            .await
            .unwrap();

        let listed = registry
            .list_platform_api_keys(&workspace.id)
            .await
            .unwrap();
        let listed_for_agent = registry
            .list_platform_api_keys_for_agent(&workspace.id, &agent.id)
            .await
            .unwrap();
        assert_eq!(
            listed,
            vec![PlatformApiKeyRecord {
                api_key: api_key.clone()
            }]
        );
        assert_eq!(
            listed_for_agent,
            vec![PlatformApiKeyRecord {
                api_key: api_key.clone()
            }]
        );

        let resolved = registry
            .get_platform_api_key_by_secret_for_agent_slug(
                &workspace.slug,
                &agent.slug,
                secret_hash,
            )
            .await
            .unwrap()
            .unwrap();
        assert_eq!(resolved.api_key, api_key);

        registry
            .touch_platform_api_key(
                &workspace.id,
                &PlatformApiKeyId::new("key_01"),
                &timestamp("2026-03-25T12:05:00Z"),
            )
            .await
            .unwrap();

        let touched = registry
            .list_platform_api_keys(&workspace.id)
            .await
            .unwrap();
        assert_eq!(
            touched[0].api_key.last_used_at,
            Some(timestamp("2026-03-25T12:05:00Z"))
        );

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_agent_read_paths() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_agent_01", 1, OperationStatus::Draft);
        let agent = test_agent("agent_01", AgentStatus::Draft);
        let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
        let bindings = vec![AgentOperationBinding {
            operation_id: operation.id.clone(),
            operation_version: operation.version,
            tool_name: "create_lead".to_owned(),
            tool_title: "Create lead".to_owned(),
            tool_description_override: Some("Creates CRM lead".to_owned()),
            enabled: true,
        }];

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_agent(CreateAgentRequest {
                agent: &agent,
                version: &version,
                bindings: &bindings,
            })
            .await
            .unwrap();

        let listed = registry.list_agents(&test_workspace_id()).await.unwrap();
        let summary = registry
            .get_agent_summary(&test_workspace_id(), &agent.id)
            .await
            .unwrap()
            .unwrap();
        let loaded_version = registry
            .get_agent_version(&test_workspace_id(), &agent.id, version.version)
            .await
            .unwrap()
            .unwrap();

        assert_eq!(listed, vec![summary.clone()]);
        assert_eq!(summary.id, agent.id);
        assert_eq!(summary.slug, agent.slug);
        assert_eq!(summary.display_name, agent.display_name);
        assert_eq!(summary.status, agent.status);
        assert_eq!(loaded_version.agent_id, agent.id);
        assert_eq!(loaded_version.version, version.version);
        assert_eq!(loaded_version.status, version.status);
        assert_eq!(loaded_version.snapshot, version);
        assert_eq!(loaded_version.bindings, bindings);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_published_agent_tool_reads() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_agent_pub_01", 1, OperationStatus::Draft);
        let operation_v2 = test_operation("op_agent_pub_01", 2, OperationStatus::Draft);
        let agent = test_agent("agent_pub_01", AgentStatus::Draft);
        let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
        let bindings = vec![AgentOperationBinding {
            operation_id: operation.id.clone(),
            operation_version: operation_v2.version,
            tool_name: "create_lead".to_owned(),
            tool_title: "Create lead".to_owned(),
            tool_description_override: Some("Creates CRM lead".to_owned()),
            enabled: true,
        }];

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_version(CreateVersionRequest {
                workspace_id: &test_workspace_id(),
                snapshot: &operation_v2,
                change_note: Some("publishable"),
                created_by: Some("alice"),
            })
            .await
            .unwrap();
        registry
            .publish_operation(PublishRequest {
                workspace_id: &test_workspace_id(),
                operation_id: &operation.id,
                version: operation_v2.version,
                published_at: &timestamp("2026-03-25T12:10:00Z"),
                published_by: Some("alice"),
            })
            .await
            .unwrap();
        registry
            .create_agent(CreateAgentRequest {
                agent: &agent,
                version: &version,
                bindings: &bindings,
            })
            .await
            .unwrap();
        registry
            .publish_agent(PublishAgentRequest {
                workspace_id: &test_workspace_id(),
                agent_id: &agent.id,
                version: version.version,
                published_at: &timestamp("2026-03-25T12:11:00Z"),
                published_by: Some("alice"),
            })
            .await
            .unwrap();

        let tools = registry
            .get_published_agent_tools_by_slug("default", &agent.slug)
            .await
            .unwrap();

        assert_eq!(tools.len(), 1);
        assert_eq!(tools[0].workspace_id, test_workspace_id());
        assert_eq!(tools[0].workspace_slug, "default");
        assert_eq!(tools[0].agent_id, agent.id);
        assert_eq!(tools[0].agent_slug, agent.slug);
        assert_eq!(tools[0].tool_name, bindings[0].tool_name);
        assert_eq!(tools[0].tool_title, bindings[0].tool_title);
        assert_eq!(tools[0].tool_description, "Creates CRM lead");
        assert_eq!(tools[0].operation.id, operation_v2.id);
        assert_eq!(tools[0].operation.version, operation_v2.version);
        assert_eq!(tools[0].operation.protocol, operation_v2.protocol);
        assert!(tools[0].operation.is_published());

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_operation_usage_and_agent_ref_reads() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_usage_01", 1, OperationStatus::Draft);
        let operation_v2 = test_operation("op_usage_01", 2, OperationStatus::Draft);
        let agent = test_agent("agent_usage_01", AgentStatus::Draft);
        let version = test_agent_version(&agent.id, 1, AgentStatus::Draft);
        let bindings = vec![AgentOperationBinding {
            operation_id: operation.id.clone(),
            operation_version: operation_v2.version,
            tool_name: "create_lead".to_owned(),
            tool_title: "Create lead".to_owned(),
            tool_description_override: None,
            enabled: true,
        }];

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_version(CreateVersionRequest {
                workspace_id: &test_workspace_id(),
                snapshot: &operation_v2,
                change_note: Some("publishable"),
                created_by: Some("alice"),
            })
            .await
            .unwrap();
        registry
            .publish_operation(PublishRequest {
                workspace_id: &test_workspace_id(),
                operation_id: &operation.id,
                version: operation_v2.version,
                published_at: &timestamp("2026-03-25T12:10:00Z"),
                published_by: Some("alice"),
            })
            .await
            .unwrap();
        registry
            .create_agent(CreateAgentRequest {
                agent: &agent,
                version: &version,
                bindings: &bindings,
            })
            .await
            .unwrap();
        registry
            .publish_agent(PublishAgentRequest {
                workspace_id: &test_workspace_id(),
                agent_id: &agent.id,
                version: version.version,
                published_at: &timestamp("2026-03-25T12:11:00Z"),
                published_by: Some("alice"),
            })
            .await
            .unwrap();
        registry
            .create_invocation_log(CreateInvocationLogRequest {
                log: &test_invocation_log(
                    "log_usage_ok",
                    &operation.id,
                    Some(agent.id.clone()),
                    crank_core::InvocationStatus::Ok,
                    120,
                    "2026-03-25T12:20:00Z",
                ),
            })
            .await
            .unwrap();
        registry
            .create_invocation_log(CreateInvocationLogRequest {
                log: &test_invocation_log(
                    "log_usage_err",
                    &operation.id,
                    Some(agent.id.clone()),
                    crank_core::InvocationStatus::Error,
                    240,
                    "2026-03-25T12:21:00Z",
                ),
            })
            .await
            .unwrap();

        let has_bindings = registry
            .has_published_agent_bindings_for_operation(&test_workspace_id(), &operation.id)
            .await
            .unwrap();
        let agent_refs = registry
            .list_operation_agent_refs(&test_workspace_id())
            .await
            .unwrap();
        let usage = registry
            .list_operation_usage_summaries(&test_workspace_id(), "2026-03-25T12:00:00Z")
            .await
            .unwrap();

        assert!(has_bindings);
        assert_eq!(agent_refs.len(), 1);
        assert_eq!(agent_refs[0].operation_id, operation.id);
        assert_eq!(agent_refs[0].agent_id, agent.id);
        assert_eq!(agent_refs[0].agent_slug, agent.slug);
        assert_eq!(agent_refs[0].display_name, agent.display_name);
        assert_eq!(usage.len(), 1);
        assert_eq!(usage[0].operation_id, operation.id);
        assert_eq!(usage[0].calls_today, 2);
        assert_eq!(usage[0].error_rate_pct, 50.0);
        assert_eq!(usage[0].avg_latency_ms, 180);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_stream_sessions_with_transitions_and_cleanup() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_stream_01", 1, OperationStatus::Draft);
        let session = test_stream_session("stream_01", "op_stream_01", StreamStatus::Running);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();

        registry
            .create_stream_session(CreateStreamSessionRequest { session: &session })
            .await
            .unwrap();

        let loaded = registry
            .get_stream_session(&session.id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(loaded.status, StreamStatus::Running);

        let closable_session =
            test_stream_session("stream_closable", "op_stream_01", StreamStatus::Running);
        registry
            .create_stream_session(CreateStreamSessionRequest {
                session: &closable_session,
            })
            .await
            .unwrap();
        registry
            .close_stream_session(&closable_session.id, &timestamp("2026-04-06T12:00:30Z"))
            .await
            .unwrap();

        let closed = registry
            .get_stream_session(&closable_session.id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(closed.status, StreamStatus::Stopped);

        let touched = registry
            .touch_stream_session_poll(&session.id, &timestamp("2026-04-06T12:00:20Z"))
            .await
            .unwrap();
        assert_eq!(
            touched.last_poll_at,
            Some(timestamp("2026-04-06T12:00:20Z"))
        );

        let updated = registry
            .update_stream_session_state(UpdateStreamSessionStateRequest {
                session_id: &session.id,
                current_status: StreamStatus::Running,
                next_status: StreamStatus::Failed,
                cursor: Some(&json!({"cursor":"next"})),
                state: &json!({"phase":"failed"}),
                expires_at: Some(&timestamp("2026-04-06T12:10:00Z")),
                last_poll_at: Some(&timestamp("2026-04-06T12:01:00Z")),
                closed_at: Some(&timestamp("2026-04-06T12:01:00Z")),
            })
            .await
            .unwrap();
        assert_eq!(updated.status, StreamStatus::Failed);
        assert_eq!(updated.closed_at, Some(timestamp("2026-04-06T12:01:00Z")));

        let page = registry
            .list_stream_sessions(StreamSessionFilter {
                workspace_id: &test_workspace_id(),
                agent_id: None,
                operation_id: None,
                status: Some(StreamStatus::Failed),
                mode: Some(crank_core::ExecutionMode::Session),
                limit: 10,
            })
            .await
            .unwrap();
        assert_eq!(page.total, 1);
        assert_eq!(page.items[0].id, session.id);

        let expired_session =
            test_stream_session("stream_expired", "op_stream_01", StreamStatus::Running);
        registry
            .create_stream_session(CreateStreamSessionRequest {
                session: &StreamSession {
                    id: expired_session.id.clone(),
                    expires_at: timestamp("2026-04-06T11:00:00Z"),
                    ..expired_session
                },
            })
            .await
            .unwrap();

        let deleted = registry
            .delete_expired_stream_sessions(&timestamp("2026-04-06T12:00:00Z"))
            .await
            .unwrap();
        assert_eq!(deleted, 1);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn rejects_invalid_stream_session_transition() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_stream_02", 1, OperationStatus::Draft);
        let session = test_stream_session("stream_invalid", "op_stream_02", StreamStatus::Stopped);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_stream_session(CreateStreamSessionRequest { session: &session })
            .await
            .unwrap();

        let error = registry
            .update_stream_session_state(UpdateStreamSessionStateRequest {
                session_id: &session.id,
                current_status: StreamStatus::Stopped,
                next_status: StreamStatus::Running,
                cursor: None,
                state: &json!({"phase":"resume"}),
                expires_at: None,
                last_poll_at: None,
                closed_at: None,
            })
            .await
            .unwrap_err();

        assert!(matches!(
            error,
            RegistryError::InvalidStreamSessionTransition { .. }
        ));

        database.cleanup().await;
    }

    #[tokio::test]
    async fn manages_async_jobs_with_transitions_and_cleanup() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_job_01", 1, OperationStatus::Draft);
        let job = test_async_job("job_01", "op_job_01", JobStatus::Running);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_async_job(CreateAsyncJobRequest { job: &job })
            .await
            .unwrap();

        let cancellable_job = test_async_job("job_cancel", "op_job_01", JobStatus::Running);
        registry
            .create_async_job(CreateAsyncJobRequest {
                job: &cancellable_job,
            })
            .await
            .unwrap();
        registry
            .cancel_async_job(&cancellable_job.id, &timestamp("2026-04-06T12:00:30Z"))
            .await
            .unwrap();

        let cancelled = registry
            .get_async_job(&cancellable_job.id)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(cancelled.status, JobStatus::Cancelled);

        let updated = registry
            .update_async_job_status(UpdateAsyncJobStatusRequest {
                job_id: &job.id,
                current_status: JobStatus::Running,
                next_status: JobStatus::Completed,
                progress: &json!({"percent":100}),
                result: Some(&json!({"ok":true})),
                error: None,
                expires_at: Some(&timestamp("2026-04-06T12:15:00Z")),
                updated_at: &timestamp("2026-04-06T12:01:00Z"),
                finished_at: Some(&timestamp("2026-04-06T12:01:00Z")),
            })
            .await
            .unwrap();
        assert_eq!(updated.status, JobStatus::Completed);
        assert_eq!(updated.result, Some(json!({"ok":true})));

        let loaded = registry.get_async_job(&job.id).await.unwrap().unwrap();
        assert_eq!(loaded.status, JobStatus::Completed);

        let touched = registry
            .touch_async_job_poll(&job.id, &timestamp("2026-04-06T12:02:00Z"))
            .await
            .unwrap();
        assert_eq!(
            touched.last_poll_at,
            Some(timestamp("2026-04-06T12:02:00Z"))
        );

        let page = registry
            .list_async_jobs(AsyncJobFilter {
                workspace_id: &test_workspace_id(),
                agent_id: None,
                operation_id: None,
                status: Some(JobStatus::Completed),
                limit: 10,
            })
            .await
            .unwrap();
        assert_eq!(page.total, 1);

        let expired_job = AsyncJobHandle {
            id: crank_core::AsyncJobId::new("job_expired"),
            expires_at: Some(timestamp("2026-04-06T11:00:00Z")),
            ..test_async_job("job_expired", "op_job_01", JobStatus::Cancelled)
        };
        registry
            .create_async_job(CreateAsyncJobRequest { job: &expired_job })
            .await
            .unwrap();

        let deleted = registry
            .delete_expired_async_jobs(&timestamp("2026-04-06T12:00:00Z"))
            .await
            .unwrap();
        assert_eq!(deleted, 1);

        database.cleanup().await;
    }

    #[tokio::test]
    async fn rejects_invalid_async_job_transition() {
        let database = TestDatabase::new().await;
        let registry = database.registry().await;
        let operation = test_operation("op_job_02", 1, OperationStatus::Draft);
        let job = test_async_job("job_invalid", "op_job_02", JobStatus::Completed);

        registry
            .create_operation(&test_workspace_id(), &operation, None)
            .await
            .unwrap();
        registry
            .create_async_job(CreateAsyncJobRequest { job: &job })
            .await
            .unwrap();

        let error = registry
            .update_async_job_status(UpdateAsyncJobStatusRequest {
                job_id: &job.id,
                current_status: JobStatus::Completed,
                next_status: JobStatus::Running,
                progress: &json!({"percent":50}),
                result: None,
                error: None,
                expires_at: None,
                updated_at: &timestamp("2026-04-06T12:01:00Z"),
                finished_at: None,
            })
            .await
            .unwrap_err();

        assert!(matches!(
            error,
            RegistryError::InvalidAsyncJobTransition { .. }
        ));

        database.cleanup().await;
    }

    struct TestDatabase {
        admin_pool: PgPool,
        database_url: String,
        schema: String,
    }

    impl TestDatabase {
        async fn new() -> Self {
            let database_url = env::var("TEST_DATABASE_URL")
                .expect("TEST_DATABASE_URL must point to a reachable PostgreSQL database");
            let admin_pool = PgPoolOptions::new()
                .max_connections(1)
                .connect(&database_url)
                .await
                .unwrap();
            let schema = format!(
                "test_registry_{}_{}",
                std::process::id(),
                SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .unwrap()
                    .as_nanos()
            );

            admin_pool
                .execute(sqlx::query(&format!("create schema {schema}")))
                .await
                .unwrap();

            Self {
                admin_pool,
                database_url,
                schema,
            }
        }

        async fn registry(&self) -> PostgresRegistry {
            PostgresRegistry::connect_in_schema(&self.database_url, Some(&self.schema))
                .await
                .unwrap()
        }

        async fn cleanup(&self) {
            self.admin_pool
                .execute(sqlx::query(&format!(
                    "drop schema if exists {} cascade",
                    self.schema
                )))
                .await
                .unwrap();
        }
    }

    fn test_operation(id: &str, version: u32, status: OperationStatus) -> RegistryOperation {
        RegistryOperation {
            id: OperationId::new(id),
            name: format!("{id}_tool"),
            display_name: format!("Display {id}"),
            category: "general".to_owned(),
            protocol: Protocol::Rest,
            security_level: OperationSecurityLevel::Standard,
            status,
            version,
            target: Target::Rest(RestTarget {
                base_url: "https://api.example.com".to_owned(),
                method: HttpMethod::Post,
                path_template: "/v1/leads".to_owned(),
                static_headers: BTreeMap::from([("X-Static".to_owned(), "true".to_owned())]),
            }),
            input_schema: Schema {
                kind: SchemaKind::Object,
                description: Some("input".to_owned()),
                required: true,
                nullable: false,
                default_value: None,
                fields: BTreeMap::from([(
                    "email".to_owned(),
                    Schema {
                        kind: SchemaKind::String,
                        description: None,
                        required: true,
                        nullable: false,
                        default_value: None,
                        fields: BTreeMap::new(),
                        items: None,
                        enum_values: Vec::new(),
                        variants: Vec::new(),
                    },
                )]),
                items: None,
                enum_values: Vec::new(),
                variants: Vec::new(),
            },
            output_schema: Schema {
                kind: SchemaKind::Object,
                description: Some("output".to_owned()),
                required: true,
                nullable: false,
                default_value: None,
                fields: BTreeMap::from([(
                    "id".to_owned(),
                    Schema {
                        kind: SchemaKind::String,
                        description: None,
                        required: true,
                        nullable: false,
                        default_value: None,
                        fields: BTreeMap::new(),
                        items: None,
                        enum_values: Vec::new(),
                        variants: Vec::new(),
                    },
                )]),
                items: None,
                enum_values: Vec::new(),
                variants: Vec::new(),
            },
            input_mapping: MappingSet {
                rules: vec![MappingRule {
                    source: "$.mcp.email".to_owned(),
                    target: "$.request.body.email".to_owned(),
                    required: true,
                    default_value: None,
                    transform: None,
                    condition: None,
                    notes: None,
                }],
            },
            output_mapping: MappingSet {
                rules: vec![MappingRule {
                    source: "$.response.body.id".to_owned(),
                    target: "$.output.id".to_owned(),
                    required: true,
                    default_value: None,
                    transform: None,
                    condition: None,
                    notes: None,
                }],
            },
            execution_config: ExecutionConfig {
                timeout_ms: 10_000,
                retry_policy: Some(RetryPolicy { max_attempts: 3 }),
                auth_profile_ref: Some("auth_crank".into()),
                headers: BTreeMap::from([("X-Request-Id".to_owned(), "static".to_owned())]),
                protocol_options: None,
                streaming: None,
            },
            tool_description: ToolDescription {
                title: "Create lead".to_owned(),
                description: "Creates CRM lead".to_owned(),
                tags: vec!["crm".to_owned()],
                examples: vec![ToolExample {
                    input: json!({ "email": "a@example.com" }),
                }],
            },
            samples: Some(Samples {
                input_json_sample_ref: Some("sample_input".into()),
                output_json_sample_ref: Some("sample_output".into()),
                proto_file_ref: None,
                descriptor_ref: None,
            }),
            generated_draft: Some(GeneratedDraft {
                status: GeneratedDraftStatus::Available,
                source_types: vec!["input_json".to_owned(), "output_json".to_owned()],
                generated_at: Some("2026-03-25T11:59:00Z".to_owned()),
                input_schema_generated: true,
                output_schema_generated: true,
                input_mapping_generated: true,
                output_mapping_generated: true,
                warnings: Vec::new(),
            }),
            config_export: Some(ConfigExport {
                format_version: "v1".to_owned(),
                export_mode: ExportMode::Portable,
            }),
            created_at: timestamp("2026-03-25T11:58:00Z"),
            updated_at: timestamp(&format!("2026-03-25T12:{version:02}:00Z")),
            published_at: None,
        }
    }

    fn test_stream_session(id: &str, operation_id: &str, status: StreamStatus) -> StreamSession {
        StreamSession {
            id: StreamSessionId::new(id),
            workspace_id: test_workspace_id(),
            agent_id: None,
            operation_id: OperationId::new(operation_id),
            protocol: Protocol::Rest,
            mode: crank_core::ExecutionMode::Session,
            status,
            cursor: Some(json!({"cursor":"initial"})),
            state: json!({"phase":"running"}),
            expires_at: timestamp("2026-04-06T12:05:00Z"),
            last_poll_at: None,
            created_at: timestamp("2026-04-06T12:00:00Z"),
            closed_at: None,
        }
    }

    fn test_agent(id: &str, status: AgentStatus) -> crank_core::Agent {
        crank_core::Agent {
            id: AgentId::new(id),
            workspace_id: test_workspace_id(),
            slug: format!("{id}_slug"),
            display_name: format!("Display {id}"),
            description: format!("Description {id}"),
            status,
            current_draft_version: 1,
            latest_published_version: None,
            created_at: timestamp("2026-03-25T11:58:00Z"),
            updated_at: timestamp("2026-03-25T12:00:00Z"),
            published_at: None,
        }
    }

    fn test_agent_version(agent_id: &AgentId, version: u32, status: AgentStatus) -> AgentVersion {
        AgentVersion {
            agent_id: agent_id.clone(),
            version,
            status,
            instructions: json!({
                "system": "triage tickets",
                "guardrails": ["don't mutate state"]
            }),
            tool_selection_policy: json!({
                "mode": "allow_list",
                "max_tools": 8
            }),
            created_at: timestamp("2026-03-25T12:00:00Z"),
        }
    }

    fn test_invocation_log(
        id: &str,
        operation_id: &OperationId,
        agent_id: Option<AgentId>,
        status: crank_core::InvocationStatus,
        duration_ms: u64,
        created_at: &str,
    ) -> InvocationLog {
        InvocationLog {
            id: crank_core::InvocationLogId::new(id),
            workspace_id: test_workspace_id(),
            agent_id,
            operation_id: operation_id.clone(),
            source: crank_core::InvocationSource::AgentToolCall,
            level: crank_core::InvocationLevel::Info,
            status,
            tool_name: "create_lead".to_owned(),
            message: "invocation".to_owned(),
            request_id: Some(format!("req_{id}")),
            status_code: Some(200),
            duration_ms,
            error_kind: None,
            request_preview: json!({"input":"value"}),
            response_preview: json!({"ok":true}),
            created_at: timestamp(created_at),
        }
    }

    fn test_async_job(id: &str, operation_id: &str, status: JobStatus) -> AsyncJobHandle {
        AsyncJobHandle {
            id: crank_core::AsyncJobId::new(id),
            workspace_id: test_workspace_id(),
            agent_id: None,
            operation_id: OperationId::new(operation_id),
            status,
            progress: json!({"percent": 25}),
            result: None,
            error: None,
            expires_at: Some(timestamp("2026-04-06T12:05:00Z")),
            last_poll_at: None,
            created_at: timestamp("2026-04-06T12:00:00Z"),
            updated_at: timestamp("2026-04-06T12:00:00Z"),
            finished_at: None,
        }
    }
}
