//! Configuration Hot-Reload Integration Tests
//!
//! Comprehensive test suite for PostgreSQL NOTIFY/LISTEN configuration hot-reload system.
//!
//! ## Test Categories
//!
//! 1. **Environment-Aware Defaults** - Verify dev/staging/prod default values
//! 2. **Environment Variable Parsing** - Test 60+ parameter overrides
//! 3. **PostgreSQL NOTIFY/LISTEN** - Hot-reload notification testing
//! 4. **Configuration Validation** - Boundary conditions and error handling
//! 5. **Service Integration** - Multi-service coordination
//!
//! ## Prerequisites
//!
//! - PostgreSQL database running
//! - Migrations applied (especially 007_configuration_schema.sql)
//! - DATABASE_URL environment variable set
//!
//! ## Running Tests
//!
//! ```bash
//! # Run all hot-reload tests
//! cargo test --test config_hot_reload --features postgres
//!
//! # Run specific category
//! cargo test --test config_hot_reload test_environment_ --features postgres
//!
//! # Run with output
//! cargo test --test config_hot_reload -- --nocapture
//! ```

use config::{DatabaseRuntimeConfig, Environment, LimitsConfig, RuntimeConfig};
use serde_json::json;
use sqlx::{Executor, PgPool};
use std::env;
use std::time::Duration;
use tokio::time::timeout;

// ============================================================================
// TEST HELPERS
// ============================================================================

/// Helper to clear and set environment variables for isolated tests
fn set_env_vars(vars: &[(&str, &str)]) {
    for (key, _) in vars {
        std::env::remove_var(key);
    }
    for (key, value) in vars {
        std::env::set_var(key, value);
    }
}

fn clear_env_vars(keys: &[&str]) {
    for key in keys {
        std::env::remove_var(key);
    }
}

/// Get database URL from environment or use default test database
fn get_database_url() -> String {
    env::var("DATABASE_URL").unwrap_or_else(|_| {
        "postgresql://postgres:postgres@localhost:5432/foxhunt_test".to_string()
    })
}

/// Create a separate connection pool for test operations
async fn create_test_pool() -> PgPool {
    let url = get_database_url();
    PgPool::connect(&url)
        .await
        .expect("Failed to create test pool")
}

/// Helper to cleanup test config settings
async fn cleanup_config_setting(pool: &PgPool, config_key: &str, environment: &str) {
    let _ = sqlx::query("DELETE FROM config_settings WHERE config_key = $1 AND environment = $2")
        .bind(config_key)
        .bind(environment)
        .execute(pool)
        .await;
}

/// Helper to cleanup config categories
async fn cleanup_config_category(pool: &PgPool, category_name: &str) {
    let _ = sqlx::query("DELETE FROM config_categories WHERE category_name = $1")
        .bind(category_name)
        .execute(pool)
        .await;
}

/// Helper to insert a test category and return its ID
async fn insert_test_category(pool: &PgPool, name: &str, path: &str) -> i32 {
    sqlx::query_scalar(
        "INSERT INTO config_categories (category_name, category_path)
         VALUES ($1, $2)
         ON CONFLICT (category_path) DO UPDATE SET category_name = EXCLUDED.category_name
         RETURNING id",
    )
    .bind(name)
    .bind(path)
    .fetch_one(pool)
    .await
    .unwrap()
}

/// Helper to insert a test config setting
async fn insert_test_config_setting(
    pool: &PgPool,
    config_key: &str,
    category_id: i32,
    category_path: &str,
    value: serde_json::Value,
    environment: &str,
) -> i32 {
    sqlx::query_scalar(
        "INSERT INTO config_settings (config_key, category_id, category_path, config_value, environment)
         VALUES ($1, $2, $3, $4, $5)
         ON CONFLICT (config_key, environment) DO UPDATE SET config_value = EXCLUDED.config_value
         RETURNING id"
    )
    .bind(config_key)
    .bind(category_id)
    .bind(category_path)
    .bind(value)
    .bind(environment)
    .fetch_one(pool)
    .await
    .unwrap()
}

// ============================================================================
// CATEGORY 1: ENVIRONMENT-AWARE DEFAULTS TESTS
// ============================================================================

/// Test: Environment detection works for explicit and default cases.
///
/// Covers "Environment-Aware Defaults" and "Environment detection from ENVIRONMENT variable".
#[test]
fn test_environment_detection_explicit() {
    // Context: Verify that setting the "ENVIRONMENT" variable correctly maps to the Environment enum.
    // Also, ensure that invalid or missing variables default to Development.
    set_env_vars(&[("ENVIRONMENT", "production")]);
    assert_eq!(
        Environment::detect(),
        Environment::Production,
        "Should detect Production environment"
    );
    clear_env_vars(&["ENVIRONMENT"]);

    set_env_vars(&[("ENVIRONMENT", "stage")]);
    assert_eq!(
        Environment::detect(),
        Environment::Staging,
        "Should detect Staging environment"
    );
    clear_env_vars(&["ENVIRONMENT"]);

    set_env_vars(&[("ENVIRONMENT", "DEV")]);
    assert_eq!(
        Environment::detect(),
        Environment::Development,
        "Should detect Development environment (case-insensitive)"
    );
    clear_env_vars(&["ENVIRONMENT"]);

    set_env_vars(&[("ENVIRONMENT", "unknown_env")]);
    assert_eq!(
        Environment::detect(),
        Environment::Development,
        "Should fallback to Development for unknown environment"
    );
    clear_env_vars(&["ENVIRONMENT"]);

    clear_env_vars(&["ENVIRONMENT"]); // Ensure it's not set
    assert_eq!(
        Environment::detect(),
        Environment::Development,
        "Should fallback to Development when ENVIRONMENT is not set"
    );
}

/// Test: All sub-configurations follow graduated defaults (dev > staging > prod tightness).
///
/// Covers "Graduated defaults (dev > staging > prod tightness)" and "Database, cache, timeout, limits configurations".
#[test]
fn test_all_subconfigs_graduated_defaults() {
    // Context: Verify that default values for different environments (Development, Staging, Production)
    // adhere to the expected "graduated defaults" principle, where Production settings are generally
    // tighter/more performant than Staging, which are tighter than Development.
    let dev = RuntimeConfig::with_defaults(Environment::Development);
    let staging = RuntimeConfig::with_defaults(Environment::Staging);
    let prod = RuntimeConfig::with_defaults(Environment::Production);

    // Database: Prod should be tighter/higher pool than Dev
    assert!(
        prod.database.query_timeout < dev.database.query_timeout,
        "Prod query timeout should be tighter than Dev"
    );
    assert!(
        prod.database.connection_timeout < dev.database.connection_timeout,
        "Prod connection timeout should be tighter than Dev"
    );
    assert!(
        prod.database.acquire_timeout < dev.database.acquire_timeout,
        "Prod acquire timeout should be tighter than Dev"
    );
    assert!(
        prod.database.pool_size > dev.database.pool_size,
        "Prod pool size should be higher than Dev"
    );
    assert!(
        prod.database.max_pool_size > dev.database.max_pool_size,
        "Prod max pool size should be higher than Dev"
    );
    assert!(
        staging.database.query_timeout < dev.database.query_timeout,
        "Staging query timeout should be tighter than Dev"
    );
    assert!(
        staging.database.query_timeout > prod.database.query_timeout,
        "Staging query timeout should be looser than Prod"
    );

    // Cache: Prod should have shorter TTLs
    assert!(
        prod.cache.position_ttl < dev.cache.position_ttl,
        "Prod position TTL should be shorter than Dev"
    );
    assert!(
        prod.cache.var_ttl < dev.cache.var_ttl,
        "Prod VaR TTL should be shorter than Dev"
    );
    assert!(
        staging.cache.position_ttl < dev.cache.position_ttl,
        "Staging position TTL should be shorter than Dev"
    );
    assert!(
        staging.cache.position_ttl > prod.cache.position_ttl,
        "Staging position TTL should be longer than Prod"
    );

    // Timeouts: Prod should have tighter network timeouts
    assert!(
        prod.timeouts.grpc_request_timeout < dev.timeouts.grpc_request_timeout,
        "Prod gRPC request timeout should be tighter than Dev"
    );
    assert!(
        prod.timeouts.keep_alive_interval < dev.timeouts.keep_alive_interval,
        "Prod keep-alive interval should be tighter than Dev"
    );
    assert!(
        staging.timeouts.grpc_request_timeout < dev.timeouts.grpc_request_timeout,
        "Staging gRPC request timeout should be tighter than Dev"
    );
    assert!(
        staging.timeouts.grpc_request_timeout > prod.timeouts.grpc_request_timeout,
        "Staging gRPC request timeout should be looser than Prod"
    );

    // Limits: Prod should have tighter safety, faster ML, lower retry attempts, tighter risk
    assert!(
        prod.limits.safety_check_timeout < dev.limits.safety_check_timeout,
        "Prod safety check timeout should be tighter than Dev"
    );
    assert!(
        prod.limits.ml_inference_timeout < dev.limits.ml_inference_timeout,
        "Prod ML inference timeout should be tighter than Dev"
    );
    assert!(
        prod.limits.retry_max_attempts < dev.limits.retry_max_attempts,
        "Prod retry max attempts should be lower than Dev"
    );
    assert!(
        prod.limits.risk_max_drawdown_warning_pct < dev.limits.risk_max_drawdown_warning_pct,
        "Prod max drawdown warning should be tighter than Dev"
    );
    assert!(
        staging.limits.safety_check_timeout < dev.limits.safety_check_timeout,
        "Staging safety check timeout should be tighter than Dev"
    );
    assert!(
        staging.limits.safety_check_timeout > prod.limits.safety_check_timeout,
        "Staging safety check timeout should be looser than Prod"
    );
}

// ============================================================================
// CATEGORY 2: ENVIRONMENT VARIABLE PARSING TESTS
// ============================================================================

/// Test: Invalid environment variable values for DatabaseRuntimeConfig lead to errors.
///
/// Covers "Invalid value error handling" and "Type validation (u32, u64, f32, f64, Duration)".
#[test]
fn test_database_config_from_env_invalid_values() {
    // Context: Ensure that parsing environment variables for database configuration
    // correctly handles invalid input types (e.g., non-numeric strings for u32).
    let keys = ["DATABASE_POOL_SIZE", "DATABASE_QUERY_TIMEOUT_MS"];
    set_env_vars(&[
        ("DATABASE_POOL_SIZE", "invalid"),
        ("DATABASE_QUERY_TIMEOUT_MS", "-100"), // Negative for u64
    ]);

    let result_pool_size = DatabaseRuntimeConfig::from_env(Environment::Production);
    assert!(
        result_pool_size.is_err(),
        "Parsing invalid string for pool size should fail"
    );
    let err_msg = result_pool_size.unwrap_err().to_string();
    assert!(
        err_msg.contains("Invalid u32 for DATABASE_POOL_SIZE"),
        "Error message should indicate invalid u32, got: {}",
        err_msg
    );

    clear_env_vars(&keys);
    set_env_vars(&[("DATABASE_QUERY_TIMEOUT_MS", "-100")]);
    let result_query_timeout = DatabaseRuntimeConfig::from_env(Environment::Production);
    assert!(
        result_query_timeout.is_err(),
        "Parsing negative duration should fail"
    );
    let err_msg = result_query_timeout.unwrap_err().to_string();
    assert!(
        err_msg.contains("Invalid duration for DATABASE_QUERY_TIMEOUT_MS"),
        "Error message should indicate invalid duration, got: {}",
        err_msg
    );

    clear_env_vars(&keys);
}

// ============================================================================
// CATEGORY 3: CONFIGURATION VALIDATION TESTS
// ============================================================================

/// Test: LimitsConfig validation handles boundary conditions and invalid ranges.
///
/// Covers "Boundary condition validation" and "Invalid configuration rejection".
#[test]
fn test_limits_config_validation_boundary_conditions() {
    // Context: Validate that the LimitsConfig's internal validation logic correctly
    // identifies and rejects configurations with invalid or out-of-bounds values.
    let mut config = LimitsConfig::with_defaults(Environment::Production);
    assert!(config.validate().is_ok(), "Default config should be valid");

    // retry_max_attempts = 0
    config.retry_max_attempts = 0;
    assert!(
        config.validate().is_err(),
        "Retry max attempts cannot be zero"
    );
    assert_eq!(
        config.validate().unwrap_err().to_string(),
        "Invalid: Retry max attempts must be positive",
        "Correct error message for zero retry attempts"
    );

    // retry_backoff_multiplier <= 1.0
    config = LimitsConfig::with_defaults(Environment::Production);
    config.retry_backoff_multiplier = 1.0;
    assert!(
        config.validate().is_err(),
        "Backoff multiplier must be > 1.0"
    );
    assert_eq!(
        config.validate().unwrap_err().to_string(),
        "Invalid: Backoff multiplier must be > 1.0",
        "Correct error message for backoff multiplier <= 1.0"
    );

    // ml_max_batch_size = 0
    config = LimitsConfig::with_defaults(Environment::Production);
    config.ml_max_batch_size = 0;
    assert!(
        config.validate().is_err(),
        "ML max batch size cannot be zero"
    );
    assert_eq!(
        config.validate().unwrap_err().to_string(),
        "Invalid: ML max batch size must be positive",
        "Correct error message for zero ML batch size"
    );

    // risk_var_confidence out of range (negative)
    config = LimitsConfig::with_defaults(Environment::Production);
    config.risk_var_confidence = -0.1;
    assert!(
        config.validate().is_err(),
        "VaR confidence cannot be negative"
    );
    assert_eq!(
        config.validate().unwrap_err().to_string(),
        "Invalid: VaR confidence must be between 0.0 and 1.0",
        "Correct error message for negative VaR confidence"
    );

    // risk_var_confidence out of range (greater than 1.0)
    config = LimitsConfig::with_defaults(Environment::Production);
    config.risk_var_confidence = 1.1;
    assert!(
        config.validate().is_err(),
        "VaR confidence cannot be greater than 1.0"
    );
    assert_eq!(
        config.validate().unwrap_err().to_string(),
        "Invalid: VaR confidence must be between 0.0 and 1.0",
        "Correct error message for VaR confidence > 1.0"
    );

    // risk_var_lookback_days = 0
    config = LimitsConfig::with_defaults(Environment::Production);
    config.risk_var_lookback_days = 0;
    assert!(
        config.validate().is_err(),
        "VaR lookback days cannot be zero"
    );
    assert_eq!(
        config.validate().unwrap_err().to_string(),
        "Invalid: VaR lookback days must be positive",
        "Correct error message for zero VaR lookback days"
    );
}

// ============================================================================
// CATEGORY 4: POSTGRESQL NOTIFY/LISTEN HOT-RELOAD TESTS
// ============================================================================

/// Test: Verify basic NOTIFY/LISTEN for config_settings table updates.
///
/// Covers "Configuration change notification trigger" and "Notification payload format validation".
#[tokio::test]
async fn test_general_config_hot_reload_notification_on_update() {
    // Context: Ensure that updating a configuration setting in the `config_settings` table
    // triggers a PostgreSQL NOTIFY event on the `foxhunt_config_changes` channel, and that
    // the payload contains the expected information about the change.
    let pool = create_test_pool().await;
    let mut listener = sqlx::postgres::PgListener::connect_with(&pool)
        .await
        .unwrap();
    listener.listen("foxhunt_config_changes").await.unwrap();

    let category_id =
        insert_test_category(&pool, "test_category_notify", "test_category_notify").await;
    let config_key = "test_setting_notify";
    let environment = "development";
    insert_test_config_setting(
        &pool,
        config_key,
        category_id,
        "test_category_notify",
        json!("initial"),
        environment,
    )
    .await;

    // Update the config setting
    sqlx::query(
        "UPDATE config_settings SET config_value = $1, updated_by = $2 WHERE config_key = $3 AND environment = $4"
    )
    .bind(json!("updated_value"))
    .bind("test_user")
    .bind(config_key)
    .bind(environment)
    .execute(&pool)
    .await
    .unwrap();

    // Wait for notification (with timeout to prevent hanging)
    let notification = timeout(Duration::from_secs(5), listener.recv())
        .await
        .unwrap()
        .unwrap();
    let payload: serde_json::Value = serde_json::from_str(notification.payload()).unwrap();

    assert_eq!(
        payload["table"], "config_settings",
        "Payload should indicate 'config_settings' table"
    );
    assert_eq!(
        payload["operation"], "UPDATE",
        "Payload should indicate 'UPDATE' operation"
    );
    assert_eq!(
        payload["config_key"], config_key,
        "Payload should contain the correct config_key"
    );
    assert_eq!(
        payload["environment"], environment,
        "Payload should contain the correct environment"
    );
    assert_eq!(
        payload["old_value"], "initial",
        "Payload should contain the old value"
    );
    assert_eq!(
        payload["new_value"], "updated_value",
        "Payload should contain the new value"
    );
    assert_eq!(
        payload["changed_by"], "test_user",
        "Payload should contain the user who made the change"
    );

    cleanup_config_setting(&pool, config_key, environment).await;
    cleanup_config_category(&pool, "test_category_notify").await;
}

// ============================================================================
// CATEGORY 5: CONCURRENT UPDATE TESTS
// ============================================================================

/// Test: Concurrent updates to config_settings using optimistic locking (version column).
///
/// Covers "Concurrent config updates maintain consistency" and "Configuration history audit trail is maintained".
#[tokio::test]
async fn test_concurrent_config_settings_updates_optimistic_locking() {
    // Context: Simulate two concurrent attempts to update the same configuration setting.
    // This test uses optimistic locking based on the `version` column to ensure that
    // only one update succeeds if both transactions read the same initial version.
    let pool = create_test_pool().await;
    let category_id = insert_test_category(&pool, "concurrent_cat", "concurrent_cat").await;
    let config_key = "concurrent_key";
    let environment = "development";
    let setting_id = insert_test_config_setting(
        &pool,
        config_key,
        category_id,
        "concurrent_cat",
        json!("initial"),
        environment,
    )
    .await;

    // Get initial version
    let initial_version: i32 =
        sqlx::query_scalar("SELECT version FROM config_settings WHERE id = $1")
            .bind(setting_id)
            .fetch_one(&pool)
            .await
            .unwrap();
    assert_eq!(initial_version, 1, "Initial version should be 1");

    let pool_clone1 = pool.clone();
    let pool_clone2 = pool.clone();

    let task1_key = config_key.to_string();
    let task1_env = environment.to_string();
    let task2_key = config_key.to_string();
    let task2_env = environment.to_string();

    // Task 1: Attempts to update the config setting
    let task1 = tokio::spawn(async move {
        // Read current version
        let current_version: i32 = sqlx::query_scalar(
            "SELECT version FROM config_settings WHERE config_key = $1 AND environment = $2",
        )
        .bind(&task1_key)
        .bind(&task1_env)
        .fetch_one(&pool_clone1)
        .await
        .unwrap();

        // Attempt update with optimistic locking (WHERE version = current_version)
        sqlx::query(
            "UPDATE config_settings
             SET config_value = $1, version = version + 1, updated_by = $2
             WHERE config_key = $3 AND environment = $4 AND version = $5",
        )
        .bind(json!("value_from_task1"))
        .bind("task1_user")
        .bind(&task1_key)
        .bind(&task1_env)
        .bind(current_version)
        .execute(&pool_clone1)
        .await
    });

    // Task 2: Attempts to update the same config setting with a small delay
    let task2 = tokio::spawn(async move {
        tokio::time::sleep(Duration::from_millis(10)).await; // Ensure task1 likely reads first
                                                             // Read current version
        let current_version: i32 = sqlx::query_scalar(
            "SELECT version FROM config_settings WHERE config_key = $1 AND environment = $2",
        )
        .bind(&task2_key)
        .bind(&task2_env)
        .fetch_one(&pool_clone2)
        .await
        .unwrap();

        // Attempt update with optimistic locking (WHERE version = current_version)
        sqlx::query(
            "UPDATE config_settings
             SET config_value = $1, version = version + 1, updated_by = $2
             WHERE config_key = $3 AND environment = $4 AND version = $5",
        )
        .bind(json!("value_from_task2"))
        .bind("task2_user")
        .bind(&task2_key)
        .bind(&task2_env)
        .bind(current_version)
        .execute(&pool_clone2)
        .await
    });

    let result1 = task1.await.unwrap().unwrap();
    let result2 = task2.await.unwrap().unwrap();

    // One update should succeed (rows_affected = 1), the other should fail (rows_affected = 0)
    assert_eq!(
        result1.rows_affected() + result2.rows_affected(),
        1,
        "Only one concurrent update should succeed with optimistic locking"
    );

    // Verify final state
    let final_value: serde_json::Value =
        sqlx::query_scalar("SELECT config_value FROM config_settings WHERE id = $1")
            .bind(setting_id)
            .fetch_one(&pool)
            .await
            .unwrap();
    let final_version: i32 =
        sqlx::query_scalar("SELECT version FROM config_settings WHERE id = $1")
            .bind(setting_id)
            .fetch_one(&pool)
            .await
            .unwrap();

    assert_eq!(
        final_version, 2,
        "Version should be incremented exactly once"
    );
    assert!(
        final_value == json!("value_from_task1") || final_value == json!("value_from_task2"),
        "Final value should be from the successful task"
    );

    cleanup_config_setting(&pool, config_key, environment).await;
    cleanup_config_category(&pool, "concurrent_cat").await;
}

// ============================================================================
// CATEGORY 6: SERVICE INTEGRATION TESTS
// ============================================================================

/// Test: RuntimeConfig::from_env() loads all configuration categories correctly.
///
/// Covers "Service Integration" and "RuntimeConfig::from_env() loads all categories".
#[test]
fn test_runtime_config_from_env_loads_all_categories() {
    // Context: Verify that RuntimeConfig::from_env() successfully loads and validates
    // all configuration categories (database, cache, timeouts, limits) using the
    // detected environment and environment variables.

    // Set up environment variables for testing
    set_env_vars(&[
        ("ENVIRONMENT", "production"),
        ("DATABASE_QUERY_TIMEOUT_MS", "500"),
        ("CACHE_POSITION_TTL_SECS", "30"),
        ("NETWORK_GRPC_REQUEST_TIMEOUT_SECS", "5"),
        ("ML_MAX_BATCH_SIZE", "4096"),
    ]);

    let result = RuntimeConfig::from_env();
    assert!(result.is_ok(), "RuntimeConfig::from_env() should succeed");

    let config = result.unwrap();
    assert_eq!(
        config.environment,
        Environment::Production,
        "Should detect Production environment"
    );

    // Verify database config was loaded
    assert_eq!(
        config.database.query_timeout,
        Duration::from_millis(500),
        "Database query timeout should be overridden by env var"
    );

    // Verify cache config was loaded
    assert_eq!(
        config.cache.position_ttl,
        Duration::from_secs(30),
        "Cache position TTL should be overridden by env var"
    );

    // Verify timeout config was loaded
    assert_eq!(
        config.timeouts.grpc_request_timeout,
        Duration::from_secs(5),
        "gRPC request timeout should be overridden by env var"
    );

    // Verify limits config was loaded
    assert_eq!(
        config.limits.ml_max_batch_size, 4096,
        "ML max batch size should be overridden by env var"
    );

    // Verify validation passed
    assert!(
        config.validate().is_ok(),
        "Config validation should pass for valid environment variables"
    );

    clear_env_vars(&[
        "ENVIRONMENT",
        "DATABASE_QUERY_TIMEOUT_MS",
        "CACHE_POSITION_TTL_SECS",
        "NETWORK_GRPC_REQUEST_TIMEOUT_SECS",
        "ML_MAX_BATCH_SIZE",
    ]);
}

/// Test: RuntimeConfig::validate() catches validation errors across all categories.
///
/// Covers "Configuration Validation" and "RuntimeConfig::validate() catches all errors".
#[test]
fn test_runtime_config_validate_catches_all_errors() {
    // Context: Verify that RuntimeConfig::validate() properly validates all sub-configurations
    // and catches errors in any category.

    let mut config = RuntimeConfig::with_defaults(Environment::Production);
    assert!(config.validate().is_ok(), "Default config should be valid");

    // Test database validation error
    config.database.query_timeout = Duration::from_millis(0);
    assert!(
        config.validate().is_err(),
        "Should catch database validation error"
    );

    // Reset and test cache validation error
    config = RuntimeConfig::with_defaults(Environment::Production);
    config.cache.position_ttl = Duration::from_secs(0);
    assert!(
        config.validate().is_err(),
        "Should catch cache validation error"
    );

    // Reset and test timeout validation error
    config = RuntimeConfig::with_defaults(Environment::Production);
    config.timeouts.grpc_connect_timeout = Duration::from_secs(0);
    assert!(
        config.validate().is_err(),
        "Should catch timeout validation error"
    );

    // Reset and test limits validation error
    config = RuntimeConfig::with_defaults(Environment::Production);
    config.limits.retry_max_attempts = 0;
    assert!(
        config.validate().is_err(),
        "Should catch limits validation error"
    );
}
