#![allow(clippy::unwrap_used, clippy::panic)] // Reason: test code, panics acceptable
use std::collections::HashMap;

use super::*;
use crate::error::ObserverError;

#[test]
fn test_observer_runtime_config_defaults() {
    let config: ObserverRuntimeConfig = serde_json::from_str("{}").unwrap();
    assert_eq!(config.channel_capacity, 1000);
    assert_eq!(config.max_concurrency, 50);
    assert_eq!(config.backlog_alert_threshold, 500);
    assert_eq!(config.shutdown_timeout, "30s");
}

#[test]
fn test_max_dlq_size_defaults_to_none() {
    let config: ObserverRuntimeConfig = serde_json::from_str("{}").unwrap();
    assert!(config.max_dlq_size.is_none());
    config
        .validate()
        .unwrap_or_else(|e| panic!("default config should be valid: {e}"));
}

#[test]
fn test_max_dlq_size_defaults_to_none_from_json() {
    let config: ObserverRuntimeConfig = serde_json::from_str("{}").unwrap();
    assert!(config.max_dlq_size.is_none());
}

#[test]
fn test_max_dlq_size_zero_is_invalid() {
    let config: ObserverRuntimeConfig = serde_json::from_str(r#"{"max_dlq_size": 0}"#).unwrap();
    assert_eq!(config.max_dlq_size, Some(0));
    assert!(config.validate().is_err(), "max_dlq_size = 0 must be rejected by validate()");
}

#[test]
fn test_max_dlq_size_positive_is_valid() {
    let config: ObserverRuntimeConfig = serde_json::from_str(r#"{"max_dlq_size": 10000}"#).unwrap();
    assert_eq!(config.max_dlq_size, Some(10_000));
    config
        .validate()
        .unwrap_or_else(|e| panic!("max_dlq_size=10000 should be valid: {e}"));
}

#[test]
fn test_max_dlq_size_one_is_valid() {
    let config: ObserverRuntimeConfig = serde_json::from_str(r#"{"max_dlq_size": 1}"#).unwrap();
    assert_eq!(config.max_dlq_size, Some(1));
    config
        .validate()
        .unwrap_or_else(|e| panic!("max_dlq_size=1 should be valid: {e}"));
}

#[test]
fn test_transport_kind_default() {
    let kind = TransportKind::default();
    assert_eq!(kind, TransportKind::Postgres);
}

#[test]
fn test_transport_config_default() {
    let config = TransportConfig::default();
    assert_eq!(config.transport, TransportKind::Postgres);
    assert!(!config.run_bridge);
    assert!(config.run_executors);
}

#[test]
fn test_transport_config_validation() {
    // Valid postgres config
    let config = TransportConfig::default();
    config
        .validate()
        .unwrap_or_else(|e| panic!("default transport config should be valid: {e}"));

    // Invalid: NATS transport without URL
    let config = TransportConfig {
        transport: TransportKind::Nats,
        nats: NatsTransportConfig {
            url: String::new(),
            ..Default::default()
        },
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "NATS without URL should fail: {result:?}"
    );

    // Invalid: run_bridge=true with postgres transport
    let config = TransportConfig {
        transport: TransportKind::Postgres,
        run_bridge: true,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "bridge with postgres should fail: {result:?}"
    );
}

#[test]
fn test_nats_transport_config_default() {
    let config = NatsTransportConfig::default();
    assert!(config.url.contains("localhost:4222"));
    assert_eq!(config.subject_prefix, "fraiseql.mutation");
    assert_eq!(config.consumer_name, "fraiseql_observer_worker");
}

#[test]
fn test_jetstream_config_default() {
    let config = JetStreamConfig::default();
    assert_eq!(config.dedup_window_minutes, 5);
    assert_eq!(config.max_age_days, 7);
    assert_eq!(config.max_msgs, 10_000_000);
    assert_eq!(config.ack_wait_secs, 30);
    assert_eq!(config.max_deliver, 3);
}

#[test]
fn test_jetstream_config_validation() {
    // Valid config
    let config = JetStreamConfig::default();
    config
        .validate()
        .unwrap_or_else(|e| panic!("default jetstream config should be valid: {e}"));

    // Invalid: dedup window = 0
    let config = JetStreamConfig {
        dedup_window_minutes: 0,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "dedup_window=0 should fail: {result:?}"
    );

    // Invalid: dedup window > 60
    let config = JetStreamConfig {
        dedup_window_minutes: 61,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "dedup_window=61 should fail: {result:?}"
    );

    // Invalid: ack_wait = 0
    let config = JetStreamConfig {
        ack_wait_secs: 0,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "ack_wait=0 should fail: {result:?}"
    );
}

#[test]
fn test_bridge_transport_config_default() {
    let config = BridgeTransportConfig::default();
    assert_eq!(config.transport_name, "pg_to_nats");
    assert_eq!(config.batch_size, 100);
    assert_eq!(config.poll_interval_secs, 1);
    assert_eq!(config.notify_channel, "fraiseql_events");
}

#[test]
fn test_bridge_transport_config_validation() {
    // Valid config
    let config = BridgeTransportConfig::default();
    config
        .validate()
        .unwrap_or_else(|e| panic!("default bridge config should be valid: {e}"));

    // Invalid: empty transport_name
    let config = BridgeTransportConfig {
        transport_name: String::new(),
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "empty transport_name should fail: {result:?}"
    );

    // Invalid: batch_size = 0
    let config = BridgeTransportConfig {
        batch_size: 0,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "batch_size=0 should fail: {result:?}"
    );

    // Invalid: batch_size > 10000
    let config = BridgeTransportConfig {
        batch_size: 10001,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "batch_size=10001 should fail: {result:?}"
    );

    // Invalid: poll_interval = 0
    let config = BridgeTransportConfig {
        poll_interval_secs: 0,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "poll_interval=0 should fail: {result:?}"
    );
}

#[test]
fn test_retry_config_defaults() {
    let config = RetryConfig::default();
    assert_eq!(config.max_attempts, 3);
    assert_eq!(config.initial_delay_ms, 100);
    assert_eq!(config.max_delay_ms, 30000);
}

#[test]
fn test_action_type_names() {
    assert_eq!(
        ActionConfig::Webhook {
            url:                None,
            url_env:            None,
            headers:            HashMap::new(),
            body_template:      None,
            signing_secret_env: None,
        }
        .action_type(),
        "webhook"
    );

    assert_eq!(
        ActionConfig::Email {
            to:               None,
            to_template:      None,
            subject:          None,
            subject_template: None,
            body_template:    None,
            reply_to:         None,
        }
        .action_type(),
        "email"
    );
}

#[test]
fn test_webhook_action_validation() {
    let invalid = ActionConfig::Webhook {
        url:                None,
        url_env:            None,
        headers:            HashMap::new(),
        body_template:      None,
        signing_secret_env: None,
    };

    let result = invalid.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidActionConfig { .. })),
        "webhook without url should fail: {result:?}"
    );

    let valid = ActionConfig::Webhook {
        url:                Some("https://example.com".to_string()),
        url_env:            None,
        headers:            HashMap::new(),
        body_template:      Some("{}".to_string()),
        signing_secret_env: None,
    };

    valid
        .validate()
        .unwrap_or_else(|e| panic!("valid webhook config should pass: {e}"));
}

#[test]
fn test_webhook_signing_secret_env_deserializes() {
    // #345: the signing-secret env-var NAME is captured from config.
    let json = serde_json::json!({
        "type": "webhook",
        "url": "https://example.com/hook",
        "signing_secret_env": "MY_WEBHOOK_SECRET"
    });
    let action: ActionConfig = serde_json::from_value(json).expect("deserialize webhook");
    match action {
        ActionConfig::Webhook {
            signing_secret_env, ..
        } => {
            assert_eq!(signing_secret_env.as_deref(), Some("MY_WEBHOOK_SECRET"));
        },
        other => panic!("expected webhook, got {other:?}"),
    }
}

#[test]
fn test_webhook_signing_secret_env_defaults_to_none() {
    let json = serde_json::json!({ "type": "webhook", "url": "https://example.com/hook" });
    let action: ActionConfig = serde_json::from_value(json).expect("deserialize webhook");
    match action {
        ActionConfig::Webhook {
            signing_secret_env, ..
        } => {
            assert_eq!(signing_secret_env, None);
        },
        other => panic!("expected webhook, got {other:?}"),
    }
}

#[test]
fn test_webhook_empty_signing_secret_env_is_rejected() {
    // An empty NAME is a config error (it would silently never resolve a secret).
    let action = ActionConfig::Webhook {
        url:                Some("https://example.com".to_string()),
        url_env:            None,
        headers:            HashMap::new(),
        body_template:      None,
        signing_secret_env: Some(String::new()),
    };
    assert!(
        matches!(action.validate(), Err(ObserverError::InvalidActionConfig { .. })),
        "empty signing_secret_env must be rejected"
    );
}

#[test]
fn test_email_action_validation() {
    let invalid = ActionConfig::Email {
        to:               None,
        to_template:      None,
        subject:          None,
        subject_template: None,
        body_template:    None,
        reply_to:         None,
    };

    let result = invalid.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidActionConfig { .. })),
        "email without fields should fail: {result:?}"
    );

    let valid = ActionConfig::Email {
        to:               Some("user@example.com".to_string()),
        to_template:      None,
        subject:          Some("Test".to_string()),
        subject_template: None,
        body_template:    Some("Body".to_string()),
        reply_to:         None,
    };

    valid
        .validate()
        .unwrap_or_else(|e| panic!("valid email config should pass: {e}"));
}

#[test]
fn test_multi_listener_config_defaults() {
    let config = MultiListenerConfig::default();
    assert!(!config.enabled);
    assert_eq!(config.lease_duration_ms, 30000);
    assert_eq!(config.health_check_interval_ms, 5000);
    assert_eq!(config.failover_threshold_ms, 60000);
    assert_eq!(config.max_listeners, 10);
}

#[test]
fn test_multi_listener_config_validation() {
    let valid_config = MultiListenerConfig::default();
    valid_config
        .validate()
        .unwrap_or_else(|e| panic!("default multi-listener config should be valid: {e}"));

    let invalid_lease = MultiListenerConfig {
        lease_duration_ms: 0,
        ..Default::default()
    };
    let result = invalid_lease.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "lease_duration=0 should fail: {result:?}"
    );

    let invalid_health_check = MultiListenerConfig {
        health_check_interval_ms: 0,
        ..Default::default()
    };
    let result = invalid_health_check.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "health_check=0 should fail: {result:?}"
    );

    let invalid_threshold = MultiListenerConfig {
        failover_threshold_ms: 1000,
        health_check_interval_ms: 5000,
        ..Default::default()
    };
    let result = invalid_threshold.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "threshold < health_check should fail: {result:?}"
    );

    let invalid_max_listeners = MultiListenerConfig {
        max_listeners: 0,
        ..Default::default()
    };
    let result = invalid_max_listeners.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "max_listeners=0 should fail: {result:?}"
    );
}

#[test]
fn test_multi_listener_config_builder() {
    let config = MultiListenerConfig::new()
        .enable()
        .with_listener_id("test-listener".to_string())
        .with_lease_duration_ms(20000);

    assert!(config.enabled);
    assert_eq!(config.listener_id, "test-listener");
    assert_eq!(config.lease_duration_ms, 20000);
}

#[test]
fn test_redis_config_defaults() {
    let config = RedisConfig::default();
    assert!(config.url.contains("localhost:6379"));
    assert_eq!(config.pool_size, 10);
    assert_eq!(config.connect_timeout_secs, 5);
    assert_eq!(config.command_timeout_secs, 2);
    assert_eq!(config.dedup_window_secs, 300);
    assert_eq!(config.cache_ttl_secs, 60);
}

#[test]
fn test_redis_config_validation() {
    // Valid config
    let config = RedisConfig::default();
    config
        .validate()
        .unwrap_or_else(|e| panic!("default redis config should be valid: {e}"));

    // Invalid: empty URL
    let config = RedisConfig {
        url: String::new(),
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "empty redis URL should fail: {result:?}"
    );

    // Invalid: pool_size = 0
    let config = RedisConfig {
        pool_size: 0,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "pool_size=0 should fail: {result:?}"
    );

    // Invalid: dedup_window too large
    let config = RedisConfig {
        dedup_window_secs: 3601,
        ..Default::default()
    };
    let result = config.validate();
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "dedup_window=3601 should fail: {result:?}"
    );
}

#[test]
fn test_performance_config_defaults() {
    let config = PerformanceConfig::default();
    assert!(!config.enable_dedup);
    assert!(!config.enable_caching);
    assert!(config.enable_concurrent);
    assert_eq!(config.max_concurrent_actions, 10);
    assert_eq!(config.concurrent_timeout_ms, 30000);
}

#[test]
fn test_performance_config_validation() {
    // Valid config (no Redis features enabled)
    let config = PerformanceConfig::default();
    config
        .validate(false)
        .unwrap_or_else(|e| panic!("default perf config should be valid: {e}"));

    // Invalid: enable_dedup without Redis
    let config = PerformanceConfig {
        enable_dedup: true,
        ..Default::default()
    };
    let result = config.validate(false);
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "dedup without redis should fail: {result:?}"
    );
    config
        .validate(true)
        .unwrap_or_else(|e| panic!("dedup with redis should pass: {e}")); // OK with Redis

    // Invalid: enable_caching without Redis
    let config = PerformanceConfig {
        enable_caching: true,
        ..Default::default()
    };
    let result = config.validate(false);
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "caching without redis should fail: {result:?}"
    );
    config
        .validate(true)
        .unwrap_or_else(|e| panic!("caching with redis should pass: {e}")); // OK with Redis

    // Invalid: max_concurrent_actions = 0
    let config = PerformanceConfig {
        max_concurrent_actions: 0,
        ..Default::default()
    };
    let result = config.validate(false);
    assert!(
        matches!(result, Err(ObserverError::InvalidConfig { .. })),
        "max_concurrent=0 should fail: {result:?}"
    );
}

// ========================================================================
// Environment variable override tests
//
// Each from_env / with_env_overrides method is tested with:
//   (a) env var set to a valid value — override is applied
//   (b) env var set to an invalid/unknown value — falls back to default
//   (c) env var unset — default is preserved
// ========================================================================

#[test]
fn transport_kind_from_env_postgres() {
    temp_env::with_vars([("FRAISEQL_OBSERVER_TRANSPORT", Some("postgres"))], || {
        assert_eq!(TransportKind::from_env(), Some(TransportKind::Postgres));
    });
}

#[test]
fn transport_kind_from_env_postgresql_alias() {
    temp_env::with_vars([("FRAISEQL_OBSERVER_TRANSPORT", Some("postgresql"))], || {
        assert_eq!(TransportKind::from_env(), Some(TransportKind::Postgres));
    });
}

#[test]
fn transport_kind_from_env_nats() {
    temp_env::with_vars([("FRAISEQL_OBSERVER_TRANSPORT", Some("nats"))], || {
        assert_eq!(TransportKind::from_env(), Some(TransportKind::Nats));
    });
}

#[test]
fn transport_kind_from_env_in_memory() {
    temp_env::with_vars([("FRAISEQL_OBSERVER_TRANSPORT", Some("in_memory"))], || {
        assert_eq!(TransportKind::from_env(), Some(TransportKind::InMemory));
    });
}

#[test]
fn transport_kind_from_env_memory_alias() {
    temp_env::with_vars([("FRAISEQL_OBSERVER_TRANSPORT", Some("memory"))], || {
        assert_eq!(TransportKind::from_env(), Some(TransportKind::InMemory));
    });
}

#[test]
fn transport_kind_from_env_unknown_returns_none() {
    temp_env::with_vars([("FRAISEQL_OBSERVER_TRANSPORT", Some("kafka"))], || {
        assert_eq!(TransportKind::from_env(), None);
    });
}

#[test]
fn transport_kind_from_env_unset_returns_none() {
    temp_env::with_vars([("FRAISEQL_OBSERVER_TRANSPORT", None::<&str>)], || {
        assert_eq!(TransportKind::from_env(), None);
    });
}

#[test]
fn redis_config_from_env_overrides_url() {
    temp_env::with_vars([("FRAISEQL_REDIS_URL", Some("redis://custom:6380"))], || {
        let cfg = RedisConfig::default().with_env_overrides();
        assert_eq!(cfg.url, "redis://custom:6380");
    });
}

#[test]
fn redis_config_from_env_overrides_pool_size() {
    temp_env::with_vars([("FRAISEQL_REDIS_POOL_SIZE", Some("25"))], || {
        let cfg = RedisConfig::default().with_env_overrides();
        assert_eq!(cfg.pool_size, 25);
    });
}

#[test]
fn redis_config_from_env_invalid_pool_size_preserves_default() {
    temp_env::with_vars([("FRAISEQL_REDIS_POOL_SIZE", Some("not_a_number"))], || {
        let default = RedisConfig::default();
        let cfg = RedisConfig::default().with_env_overrides();
        assert_eq!(cfg.pool_size, default.pool_size);
    });
}

#[test]
fn redis_config_from_env_url_unset_preserves_default() {
    temp_env::with_vars([("FRAISEQL_REDIS_URL", None::<&str>)], || {
        let default = RedisConfig::default();
        let cfg = RedisConfig::default().with_env_overrides();
        assert_eq!(cfg.url, default.url);
    });
}

#[test]
fn nats_transport_config_from_env_overrides_url() {
    temp_env::with_vars([("FRAISEQL_NATS_URL", Some("nats://custom-nats:4222"))], || {
        let cfg = NatsTransportConfig::default().with_env_overrides();
        assert_eq!(cfg.url, "nats://custom-nats:4222");
    });
}

#[test]
fn nats_transport_config_from_env_overrides_subject_prefix() {
    temp_env::with_vars([("FRAISEQL_NATS_SUBJECT_PREFIX", Some("myapp.events"))], || {
        let cfg = NatsTransportConfig::default().with_env_overrides();
        assert_eq!(cfg.subject_prefix, "myapp.events");
    });
}

#[test]
fn nats_transport_config_from_env_overrides_consumer_name() {
    temp_env::with_vars([("FRAISEQL_NATS_CONSUMER_NAME", Some("my_consumer"))], || {
        let cfg = NatsTransportConfig::default().with_env_overrides();
        assert_eq!(cfg.consumer_name, "my_consumer");
    });
}

#[test]
fn nats_transport_config_from_env_url_unset_preserves_default() {
    temp_env::with_vars([("FRAISEQL_NATS_URL", None::<&str>)], || {
        let default = NatsTransportConfig::default();
        let cfg = NatsTransportConfig::default().with_env_overrides();
        assert_eq!(cfg.url, default.url);
    });
}

#[test]
fn jetstream_config_from_env_overrides_dedup_window() {
    temp_env::with_vars([("FRAISEQL_NATS_DEDUP_WINDOW_MINUTES", Some("10"))], || {
        let cfg = JetStreamConfig::default().with_env_overrides();
        assert_eq!(cfg.dedup_window_minutes, 10);
    });
}

#[test]
fn jetstream_config_from_env_overrides_max_age_days() {
    temp_env::with_vars([("FRAISEQL_NATS_MAX_AGE_DAYS", Some("14"))], || {
        let cfg = JetStreamConfig::default().with_env_overrides();
        assert_eq!(cfg.max_age_days, 14);
    });
}

#[test]
fn jetstream_config_from_env_unset_preserves_defaults() {
    temp_env::with_vars(
        [
            ("FRAISEQL_NATS_DEDUP_WINDOW_MINUTES", None::<&str>),
            ("FRAISEQL_NATS_MAX_AGE_DAYS", None::<&str>),
        ],
        || {
            let default = JetStreamConfig::default();
            let cfg = JetStreamConfig::default().with_env_overrides();
            assert_eq!(cfg.dedup_window_minutes, default.dedup_window_minutes);
            assert_eq!(cfg.max_age_days, default.max_age_days);
        },
    );
}

#[test]
fn job_queue_config_from_env_overrides_batch_size() {
    temp_env::with_vars([("FRAISEQL_JOB_QUEUE_BATCH_SIZE", Some("200"))], || {
        let cfg = JobQueueConfig::default().with_env_overrides();
        assert_eq!(cfg.batch_size, 200);
    });
}

#[test]
fn job_queue_config_from_env_overrides_worker_concurrency() {
    temp_env::with_vars([("FRAISEQL_JOB_QUEUE_WORKER_CONCURRENCY", Some("8"))], || {
        let cfg = JobQueueConfig::default().with_env_overrides();
        assert_eq!(cfg.worker_concurrency, 8);
    });
}

#[test]
fn job_queue_config_from_env_unset_preserves_defaults() {
    temp_env::with_vars([("FRAISEQL_JOB_QUEUE_BATCH_SIZE", None::<&str>)], || {
        let default = JobQueueConfig::default();
        let cfg = JobQueueConfig::default().with_env_overrides();
        assert_eq!(cfg.batch_size, default.batch_size);
    });
}

#[test]
fn performance_config_from_env_enables_dedup() {
    temp_env::with_vars([("FRAISEQL_ENABLE_DEDUP", Some("true"))], || {
        let cfg = PerformanceConfig::default().with_env_overrides();
        assert!(cfg.enable_dedup, "FRAISEQL_ENABLE_DEDUP=true should enable dedup");
    });
}

#[test]
fn performance_config_from_env_enables_caching() {
    temp_env::with_vars([("FRAISEQL_ENABLE_CACHING", Some("1"))], || {
        let cfg = PerformanceConfig::default().with_env_overrides();
        assert!(cfg.enable_caching, "FRAISEQL_ENABLE_CACHING=1 should enable caching");
    });
}

#[test]
fn performance_config_from_env_overrides_max_concurrent_actions() {
    temp_env::with_vars([("FRAISEQL_MAX_CONCURRENT_ACTIONS", Some("20"))], || {
        let cfg = PerformanceConfig::default().with_env_overrides();
        assert_eq!(cfg.max_concurrent_actions, 20);
    });
}

#[test]
fn performance_config_from_env_unset_preserves_defaults() {
    temp_env::with_vars(
        [
            ("FRAISEQL_ENABLE_DEDUP", None::<&str>),
            ("FRAISEQL_ENABLE_CACHING", None::<&str>),
            ("FRAISEQL_MAX_CONCURRENT_ACTIONS", None::<&str>),
        ],
        || {
            let default = PerformanceConfig::default();
            let cfg = PerformanceConfig::default().with_env_overrides();
            assert_eq!(cfg.enable_dedup, default.enable_dedup);
            assert_eq!(cfg.enable_caching, default.enable_caching);
            assert_eq!(cfg.max_concurrent_actions, default.max_concurrent_actions);
        },
    );
}

#[test]
fn clickhouse_config_from_env_overrides_url() {
    temp_env::with_vars([("FRAISEQL_CLICKHOUSE_URL", Some("http://ch-server:8123"))], || {
        let cfg = ClickHouseConfig::default().with_env_overrides();
        assert_eq!(cfg.url, "http://ch-server:8123");
    });
}

#[test]
fn clickhouse_config_from_env_overrides_database() {
    temp_env::with_vars([("FRAISEQL_CLICKHOUSE_DATABASE", Some("analytics"))], || {
        let cfg = ClickHouseConfig::default().with_env_overrides();
        assert_eq!(cfg.database, "analytics");
    });
}

#[test]
fn clickhouse_config_from_env_unset_preserves_defaults() {
    temp_env::with_vars(
        [
            ("FRAISEQL_CLICKHOUSE_URL", None::<&str>),
            ("FRAISEQL_CLICKHOUSE_DATABASE", None::<&str>),
        ],
        || {
            let default = ClickHouseConfig::default();
            let cfg = ClickHouseConfig::default().with_env_overrides();
            assert_eq!(cfg.url, default.url);
            assert_eq!(cfg.database, default.database);
        },
    );
}

#[test]
fn transport_config_from_env_overrides_kind() {
    temp_env::with_vars([("FRAISEQL_OBSERVER_TRANSPORT", Some("nats"))], || {
        let cfg = TransportConfig::default().with_env_overrides();
        assert_eq!(cfg.transport, TransportKind::Nats);
    });
}

#[test]
fn transport_config_from_env_enables_bridge() {
    temp_env::with_vars(
        [
            ("FRAISEQL_OBSERVER_TRANSPORT", Some("nats")),
            ("FRAISEQL_NATS_ENABLE_BRIDGE", Some("true")),
        ],
        || {
            let cfg = TransportConfig::default().with_env_overrides();
            assert!(cfg.run_bridge, "FRAISEQL_NATS_ENABLE_BRIDGE=true should enable bridge");
        },
    );
}

#[test]
fn transport_config_from_env_unset_preserves_defaults() {
    temp_env::with_vars(
        [
            ("FRAISEQL_OBSERVER_TRANSPORT", None::<&str>),
            ("FRAISEQL_NATS_ENABLE_BRIDGE", None::<&str>),
        ],
        || {
            let default = TransportConfig::default();
            let cfg = TransportConfig::default().with_env_overrides();
            assert_eq!(cfg.transport, default.transport);
            assert_eq!(cfg.run_bridge, default.run_bridge);
        },
    );
}

// ── H24: unimplemented action types are rejected at config-load time ──
//
// SMS / Push / Search / Cache had no real transport wired — dispatch fabricated
// `success: true` and sent nothing. They must now fail loud, starting at
// `validate()` so a misconfigured observer refuses to start.

#[test]
fn test_sms_action_config_is_rejected_as_unsupported() {
    let action = ActionConfig::Sms {
        phone:            Some("+15551234567".to_string()),
        phone_template:   None,
        message_template: Some("hello".to_string()),
    };
    assert!(
        matches!(action.validate(), Err(ObserverError::UnsupportedActionType { .. })),
        "a well-formed SMS action config must be rejected as unsupported (H24)"
    );
}

#[test]
fn test_push_action_config_is_rejected_as_unsupported() {
    let action = ActionConfig::Push {
        device_token:   Some("token".to_string()),
        title_template: Some("title".to_string()),
        body_template:  Some("body".to_string()),
    };
    assert!(
        matches!(action.validate(), Err(ObserverError::UnsupportedActionType { .. })),
        "a well-formed Push action config must be rejected as unsupported (H24)"
    );
}

#[test]
fn test_search_action_config_is_rejected_as_unsupported() {
    let action = ActionConfig::Search {
        index:       "users".to_string(),
        id_template: Some("user_{{ id }}".to_string()),
    };
    assert!(
        matches!(action.validate(), Err(ObserverError::UnsupportedActionType { .. })),
        "a well-formed Search action config must be rejected as unsupported (H24)"
    );
}

// ── #428: cache invalidation now has a real Redis transport ──
//
// A well-formed `cache`/invalidate config validates (transport availability is
// enforced at dispatch, like the email action). `refresh` and an empty pattern
// still fail loud at config-load.

#[test]
fn test_cache_invalidate_action_config_is_accepted() {
    let action = ActionConfig::Cache {
        key_pattern: "app:user:{{ id }}".to_string(),
        action:      "invalidate".to_string(),
    };
    assert!(
        action.validate().is_ok(),
        "a well-formed cache/invalidate action config must validate (#428)"
    );
}

#[test]
fn test_cache_refresh_action_config_is_rejected() {
    let action = ActionConfig::Cache {
        key_pattern: "user:*".to_string(),
        action:      "refresh".to_string(),
    };
    assert!(
        matches!(action.validate(), Err(ObserverError::InvalidActionConfig { .. })),
        "cache 'refresh' has no transport and must fail loud (#428)"
    );
}

#[test]
fn test_cache_action_config_rejects_empty_key_pattern() {
    let action = ActionConfig::Cache {
        key_pattern: String::new(),
        action:      "invalidate".to_string(),
    };
    assert!(
        matches!(action.validate(), Err(ObserverError::InvalidActionConfig { .. })),
        "an empty key_pattern must fail loud (#428)"
    );
}
