horizon-sdk 16.0.0

Canonical Rust data access layer for the Horizon platform
//! Tests for the `HorizonSdk` builder and facade.
//!
//! Unit-level tests for builder configuration. Integration tests for
//! dual-write require live Postgres, Kafka, and Iceberg services.

#![allow(
    clippy::absolute_paths,
    clippy::assertions_on_result_states,
    clippy::expect_used,
    clippy::panic_in_result_fn,
    clippy::single_call_fn,
    clippy::tests_outside_test_module,
    clippy::unwrap_used,
    reason = "integration tests use assertions, unwrap, and expect freely"
)]

use horizon_sdk::HorizonSdk;
use horizon_sdk::types::config::{CoverageConfig, IcebergConfig, PostgresConfig, SdkConfig};
use horizon_sdk::types::error::HorizonError;

// ---------------------------------------------------------------------------
// Builder validation
// ---------------------------------------------------------------------------

#[tokio::test]
async fn builder_fails_without_postgres() {
    let result = HorizonSdk::builder().build().await;
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("Postgres configuration is required"));
}

#[tokio::test]
async fn builder_with_invalid_postgres_url_fails() {
    let result = HorizonSdk::builder()
        .with_postgres_url("not-a-url")
        .build()
        .await;
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(err.contains("invalid Postgres URL"));
}

#[tokio::test]
async fn builder_iceberg_without_kafka_fails() {
    let config = SdkConfig {
        horizon_api: None,
        coverage: None,
        iceberg: Some(IcebergConfig {
            catalog_uri: "http://localhost:8181".to_owned(),
            catalog_warehouse: None,
            data_row_table: "horizon.data_row".to_owned(),
            metadata_row_table: "horizon.metadata_row".to_owned(),
            credential: None,
            oauth2_server_uri: None,
            scope: None,
        }),
        kafka: None,
        postgres: test_postgres_config(),
    };

    let result = HorizonSdk::builder().with_config(config).build().await;
    assert!(result.is_err());
    let err = result.unwrap_err().to_string();
    assert!(
        err.contains("Kafka"),
        "error should mention Kafka subsystem: {err}"
    );
    assert!(
        err.contains("Iceberg"),
        "error should mention Iceberg dependency: {err}"
    );
}

// ---------------------------------------------------------------------------
// Config loading
// ---------------------------------------------------------------------------

#[test]
fn load_config_fails_for_missing_file() {
    let result = horizon_sdk::load_config(Some("/nonexistent/config.toml"));
    assert!(result.is_err());
}

// ---------------------------------------------------------------------------
// Facade: Postgres-only build (requires live DB)
// ---------------------------------------------------------------------------

#[tokio::test]
#[ignore = "requires live Postgres"]
async fn build_with_postgres_only() -> std::result::Result<(), HorizonError> {
    let sdk = HorizonSdk::builder()
        .with_postgres_config(live_postgres_config())
        .build()
        .await?;

    assert!(!sdk.has_iceberg());
    assert!(sdk.coverage().is_none());

    let debug = format!("{sdk:?}");
    assert!(debug.contains("HorizonSdk"));
    assert!(debug.contains("iceberg_enabled: false"));
    Ok(())
}

#[tokio::test]
#[ignore = "requires live Postgres"]
async fn build_with_postgres_and_coverage() -> std::result::Result<(), HorizonError> {
    let sdk = HorizonSdk::builder()
        .with_postgres_config(live_postgres_config())
        .with_coverage(CoverageConfig {
            api_key: None,
            base_url: "http://localhost:9999".to_owned(),
            timeout_secs: 5,
        })
        .build()
        .await?;

    assert!(sdk.coverage().is_some());
    assert_eq!(sdk.coverage().unwrap().base_url(), "http://localhost:9999");
    Ok(())
}

#[tokio::test]
#[ignore = "requires live Postgres"]
async fn build_with_postgres_url() -> std::result::Result<(), HorizonError> {
    let url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
        "postgresql://horizon_owner:insecure_password@localhost:5432/horizon?sslmode=disable"
            .to_owned()
    });
    let sdk = HorizonSdk::builder()
        .with_postgres_url(&url)
        .build()
        .await?;

    assert!(!sdk.has_iceberg());
    Ok(())
}

#[tokio::test]
#[ignore = "requires live Postgres"]
async fn facade_delegates_to_postgres_repo() -> std::result::Result<(), HorizonError> {
    let sdk = HorizonSdk::builder()
        .with_postgres_config(live_postgres_config())
        .build()
        .await?;

    let _platforms = sdk.postgres().list_platforms().await?;
    Ok(())
}

#[tokio::test]
#[ignore = "requires live Postgres"]
async fn build_with_organization_id() -> std::result::Result<(), HorizonError> {
    let org_id = uuid::Uuid::new_v4();
    let sdk = HorizonSdk::builder()
        .with_postgres_config(live_postgres_config())
        .with_organization_id(org_id)
        .build()
        .await?;

    assert!(!sdk.has_iceberg());
    Ok(())
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn test_postgres_config() -> PostgresConfig {
    PostgresConfig {
        database: "horizon".to_owned(),
        host: "localhost".to_owned(),
        password: "test".to_owned(),
        port: 5432,
        possible_organization_id: None,
        role: None,
        sslmode: "disable".to_owned(),
        user: "test".to_owned(),
    }
}

fn live_postgres_config() -> PostgresConfig {
    let url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
        "postgresql://horizon_owner:insecure_password@localhost:5432/horizon?sslmode=disable"
            .to_owned()
    });
    let parsed = url::Url::parse(&url).expect("valid DATABASE_URL");
    PostgresConfig {
        database: parsed.path().trim_start_matches('/').to_owned(),
        host: parsed.host_str().unwrap_or("localhost").to_owned(),
        password: parsed.password().unwrap_or("insecure_password").to_owned(),
        port: parsed.port().unwrap_or(5432),
        possible_organization_id: None,
        role: None,
        sslmode: "disable".to_owned(),
        user: if parsed.username().is_empty() {
            "horizon_owner".to_owned()
        } else {
            parsed.username().to_owned()
        },
    }
}