use super::*;
use std::collections::HashMap;
use std::fs;
use tempfile::TempDir;

#[cfg(unix)]
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());

#[test]
fn test_empty_config() {
    let config = Config::new();
    assert!(config.node.identity.nsec.is_none());
    assert!(!config.has_identity());
}

#[test]
fn test_parse_yaml_with_nsec() {
    let yaml = r#"
node:
  identity:
    nsec: nsec1qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxfnm5g9
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();
    assert!(config.node.identity.nsec.is_some());
    assert!(config.has_identity());
}

#[test]
fn test_parse_yaml_with_hex() {
    let yaml = r#"
node:
  identity:
    nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();
    assert!(config.node.identity.nsec.is_some());

    let identity = config.create_identity().unwrap();
    assert!(!identity.npub().is_empty());
}

#[test]
fn test_parse_yaml_empty() {
    let yaml = "";
    let config: Config = serde_yaml::from_str(yaml).unwrap();
    assert!(config.node.identity.nsec.is_none());
}

#[test]
fn test_parse_yaml_partial() {
    let yaml = r#"
node:
  identity: {}
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();
    assert!(config.node.identity.nsec.is_none());
}

#[test]
fn test_merge_configs() {
    let mut base = Config::new();
    base.node.identity.nsec = Some("base_nsec".to_string());

    let mut override_config = Config::new();
    override_config.node.identity.nsec = Some("override_nsec".to_string());

    base.merge(override_config);
    assert_eq!(base.node.identity.nsec, Some("override_nsec".to_string()));
}

#[test]
fn test_merge_preserves_base_when_override_empty() {
    let mut base = Config::new();
    base.node.identity.nsec = Some("base_nsec".to_string());

    let override_config = Config::new();

    base.merge(override_config);
    assert_eq!(base.node.identity.nsec, Some("base_nsec".to_string()));
}

#[test]
fn test_create_identity_from_nsec() {
    let mut config = Config::new();
    config.node.identity.nsec =
        Some("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string());

    let identity = config.create_identity().unwrap();
    assert!(!identity.npub().is_empty());
}

#[test]
fn test_create_identity_generates_new() {
    let config = Config::new();
    let identity = config.create_identity().unwrap();
    assert!(!identity.npub().is_empty());
}

#[test]
fn test_load_from_file() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("fips.yaml");

    let yaml = r#"
node:
  identity:
    nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
"#;
    fs::write(&config_path, yaml).unwrap();

    let config = Config::load_file(&config_path).unwrap();
    assert!(config.node.identity.nsec.is_some());
}

#[test]
fn test_load_from_paths_merges() {
    let temp_dir = TempDir::new().unwrap();

    // Create two config files
    let low_priority = temp_dir.path().join("low.yaml");
    let high_priority = temp_dir.path().join("high.yaml");

    fs::write(
        &low_priority,
        r#"
node:
  identity:
    nsec: "low_priority_nsec"
"#,
    )
    .unwrap();

    fs::write(
        &high_priority,
        r#"
node:
  identity:
    nsec: "high_priority_nsec"
"#,
    )
    .unwrap();

    let paths = vec![low_priority.clone(), high_priority.clone()];
    let (config, loaded) = Config::load_from_paths(&paths).unwrap();

    assert_eq!(loaded.len(), 2);
    assert_eq!(
        config.node.identity.nsec,
        Some("high_priority_nsec".to_string())
    );
}

#[test]
fn test_load_from_paths_deep_merges_partial_node_sections() {
    let temp_dir = TempDir::new().unwrap();
    let low_priority = temp_dir.path().join("low.yaml");
    let high_priority = temp_dir.path().join("high.yaml");

    fs::write(
        &low_priority,
        r#"
node:
  limits:
    max_peers: 4096
  routing:
    learned_ttl_secs: 120
    max_learned_routes_per_dest: 2
"#,
    )
    .unwrap();

    fs::write(
        &high_priority,
        r#"
node:
  identity:
    nsec: "high_priority_nsec"
  routing:
    learned_fallback_explore_interval: 8
"#,
    )
    .unwrap();

    let (config, loaded) =
        Config::load_from_paths(&[low_priority.clone(), high_priority.clone()]).unwrap();

    assert_eq!(loaded, vec![low_priority, high_priority]);
    assert_eq!(config.node.limits.max_peers, 4096);
    assert_eq!(config.node.routing.learned_ttl_secs, 120);
    assert_eq!(config.node.routing.max_learned_routes_per_dest, 2);
    assert_eq!(config.node.routing.learned_fallback_explore_interval, 8);
    assert_eq!(
        config.node.identity.nsec,
        Some("high_priority_nsec".to_string())
    );
}

#[test]
fn test_load_skips_missing_files() {
    let temp_dir = TempDir::new().unwrap();
    let existing = temp_dir.path().join("exists.yaml");
    let missing = temp_dir.path().join("missing.yaml");

    fs::write(
        &existing,
        r#"
node:
  identity:
    nsec: "existing_nsec"
"#,
    )
    .unwrap();

    let paths = vec![missing, existing.clone()];
    let (config, loaded) = Config::load_from_paths(&paths).unwrap();

    assert_eq!(loaded.len(), 1);
    assert_eq!(loaded[0], existing);
    assert_eq!(config.node.identity.nsec, Some("existing_nsec".to_string()));
}

#[test]
fn test_search_paths_includes_expected() {
    let paths = Config::search_paths();

    // Should include current directory
    assert!(paths.iter().any(|p| p.ends_with("fips.yaml")));

    // Should include /etc/fips on Unix
    #[cfg(unix)]
    assert!(
        paths
            .iter()
            .any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml"))
    );
}

#[test]
fn test_to_yaml() {
    let mut config = Config::new();
    config.node.identity.nsec = Some("test_nsec".to_string());

    let yaml = config.to_yaml().unwrap();
    assert!(yaml.contains("node:"));
    assert!(yaml.contains("identity:"));
    assert!(yaml.contains("nsec:"));
    assert!(yaml.contains("test_nsec"));
}

#[test]
fn test_key_file_write_read_roundtrip() {
    let temp_dir = TempDir::new().unwrap();
    let key_path = temp_dir.path().join("fips.key");

    let identity = crate::Identity::generate();
    let nsec = crate::encode_nsec(&identity.keypair().secret_key());

    write_key_file(&key_path, &nsec).unwrap();

    let loaded_nsec = read_key_file(&key_path).unwrap();
    assert_eq!(loaded_nsec, nsec);

    // Verify the loaded nsec produces the same identity
    let loaded_identity = crate::Identity::from_secret_str(&loaded_nsec).unwrap();
    assert_eq!(loaded_identity.npub(), identity.npub());
}

#[cfg(unix)]
#[test]
fn test_key_file_permissions() {
    use std::os::unix::fs::MetadataExt;

    let temp_dir = TempDir::new().unwrap();
    let key_path = temp_dir.path().join("fips.key");

    write_key_file(&key_path, "nsec1test").unwrap();

    let metadata = fs::metadata(&key_path).unwrap();
    assert_eq!(metadata.mode() & 0o777, 0o600);
}

#[cfg(unix)]
#[test]
fn test_key_file_permissions_are_tightened_on_overwrite() {
    use std::os::unix::fs::{MetadataExt, PermissionsExt};

    let temp_dir = TempDir::new().unwrap();
    let key_path = temp_dir.path().join("fips.key");
    fs::write(&key_path, "old\n").unwrap();
    fs::set_permissions(&key_path, fs::Permissions::from_mode(0o644)).unwrap();

    write_key_file(&key_path, "nsec1test").unwrap();

    let metadata = fs::metadata(&key_path).unwrap();
    assert_eq!(metadata.mode() & 0o777, 0o600);
    assert_eq!(read_key_file(&key_path).unwrap(), "nsec1test");
}

#[cfg(unix)]
#[test]
fn test_pub_file_permissions() {
    use std::os::unix::fs::MetadataExt;

    let temp_dir = TempDir::new().unwrap();
    let pub_path = temp_dir.path().join("fips.pub");

    write_pub_file(&pub_path, "npub1test").unwrap();

    let metadata = fs::metadata(&pub_path).unwrap();
    assert_eq!(metadata.mode() & 0o777, 0o644);
}

#[test]
fn test_key_file_empty_error() {
    let temp_dir = TempDir::new().unwrap();
    let key_path = temp_dir.path().join("fips.key");

    fs::write(&key_path, "").unwrap();

    let result = read_key_file(&key_path);
    assert!(result.is_err());
    assert!(result.unwrap_err().to_string().contains("empty"));
}

#[test]
fn test_key_file_whitespace_trimmed() {
    let temp_dir = TempDir::new().unwrap();
    let key_path = temp_dir.path().join("fips.key");

    fs::write(&key_path, "  nsec1test  \n").unwrap();

    let nsec = read_key_file(&key_path).unwrap();
    assert_eq!(nsec, "nsec1test");
}

#[test]
fn test_key_file_path_derivation() {
    let config_path = PathBuf::from("/etc/fips/fips.yaml");
    assert_eq!(
        key_file_path(&config_path),
        PathBuf::from("/etc/fips/fips.key")
    );
    assert_eq!(
        pub_file_path(&config_path),
        PathBuf::from("/etc/fips/fips.pub")
    );
}

#[cfg(windows)]
#[test]
fn test_key_file_write_read_roundtrip_windows() {
    let temp_dir = TempDir::new().unwrap();
    let key_path = temp_dir.path().join("fips.key");

    let identity = crate::Identity::generate();
    let nsec = crate::encode_nsec(&identity.keypair().secret_key());

    write_key_file(&key_path, &nsec).unwrap();

    // Verify file was created and can be read back
    let loaded_nsec = read_key_file(&key_path).unwrap();
    assert_eq!(loaded_nsec, nsec);

    // Verify the loaded nsec produces the same identity
    let loaded_identity = crate::Identity::from_secret_str(&loaded_nsec).unwrap();
    assert_eq!(loaded_identity.npub(), identity.npub());
}

#[test]
fn test_resolve_identity_from_config() {
    let mut config = Config::new();
    config.node.identity.nsec =
        Some("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string());

    let resolved = resolve_identity(&config, &[]).unwrap();
    assert!(matches!(resolved.source, IdentitySource::Config));
}

#[test]
fn test_resolve_identity_ephemeral_by_default() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("fips.yaml");

    fs::write(&config_path, "node:\n  identity: {}\n").unwrap();

    let config = Config::load_file(&config_path).unwrap();
    assert!(!config.node.identity.persistent);

    let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
    assert!(matches!(resolved.source, IdentitySource::Ephemeral));

    // Key files should still be written for operator visibility
    let key_path = temp_dir.path().join("fips.key");
    let pub_path = temp_dir.path().join("fips.pub");
    assert!(key_path.exists());
    assert!(pub_path.exists());
}

#[test]
fn test_resolve_identity_ephemeral_changes_each_call() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("fips.yaml");

    fs::write(&config_path, "node:\n  identity: {}\n").unwrap();

    let config = Config::load_file(&config_path).unwrap();
    let first = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
    let second = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();

    // Each call generates a different key
    assert_ne!(first.nsec, second.nsec);
}

#[test]
fn test_resolve_identity_persistent_from_key_file() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("fips.yaml");
    let key_path = temp_dir.path().join("fips.key");

    fs::write(&config_path, "node:\n  identity:\n    persistent: true\n").unwrap();

    // Write a key file
    let identity = crate::Identity::generate();
    let nsec = crate::encode_nsec(&identity.keypair().secret_key());
    write_key_file(&key_path, &nsec).unwrap();

    let config = Config::load_file(&config_path).unwrap();
    assert!(config.node.identity.persistent);

    let resolved = resolve_identity(&config, &[config_path]).unwrap();
    assert!(matches!(resolved.source, IdentitySource::KeyFile(_)));
    assert_eq!(resolved.nsec, nsec);
}

#[test]
fn test_resolve_identity_persistent_generates_and_persists() {
    let temp_dir = TempDir::new().unwrap();
    let config_path = temp_dir.path().join("fips.yaml");

    fs::write(&config_path, "node:\n  identity:\n    persistent: true\n").unwrap();

    let config = Config::load_file(&config_path).unwrap();
    let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();

    assert!(matches!(resolved.source, IdentitySource::Generated(_)));

    // Key file and pub file should now exist
    let key_path = temp_dir.path().join("fips.key");
    let pub_path = temp_dir.path().join("fips.pub");
    assert!(key_path.exists());
    assert!(pub_path.exists());

    // Second resolve should load from key file (not generate new)
    let resolved2 = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
    assert!(matches!(resolved2.source, IdentitySource::KeyFile(_)));
    assert_eq!(resolved.nsec, resolved2.nsec);
}

#[test]
fn test_to_yaml_empty_nsec_omitted() {
    let config = Config::new();
    let yaml = config.to_yaml().unwrap();

    // Empty nsec should not be serialized
    assert!(!yaml.contains("nsec:"));
}

#[test]
fn test_parse_transport_single_instance() {
    let yaml = r#"
transports:
  udp:
    bind_addr: "0.0.0.0:2121"
    mtu: 1400
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();

    assert_eq!(config.transports.udp.len(), 1);
    let instances: Vec<_> = config.transports.udp.iter().collect();
    assert_eq!(instances.len(), 1);
    assert_eq!(instances[0].0, None); // Single instance has no name
    assert_eq!(instances[0].1.bind_addr(), "0.0.0.0:2121");
    assert_eq!(instances[0].1.mtu(), 1400);
}

#[test]
fn test_parse_transport_named_instances() {
    let yaml = r#"
transports:
  udp:
    main:
      bind_addr: "0.0.0.0:2121"
    backup:
      bind_addr: "192.168.1.100:2122"
      mtu: 1280
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();

    assert_eq!(config.transports.udp.len(), 2);

    let instances: std::collections::HashMap<_, _> = config.transports.udp.iter().collect();

    // Named instances have Some(name)
    assert!(instances.contains_key(&Some("main")));
    assert!(instances.contains_key(&Some("backup")));
    assert_eq!(instances[&Some("main")].bind_addr(), "0.0.0.0:2121");
    assert_eq!(instances[&Some("backup")].bind_addr(), "192.168.1.100:2122");
    assert_eq!(instances[&Some("backup")].mtu(), 1280);
}

#[test]
fn test_parse_transport_empty() {
    let yaml = r#"
transports: {}
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();
    assert!(config.transports.udp.is_empty());
    assert!(config.transports.is_empty());
}

#[test]
fn test_transport_instances_iter() {
    // Single instance - no name
    let single = TransportInstances::Single(UdpConfig {
        bind_addr: Some("0.0.0.0:2121".to_string()),
        mtu: None,
        ..Default::default()
    });
    let items: Vec<_> = single.iter().collect();
    assert_eq!(items.len(), 1);
    assert_eq!(items[0].0, None);

    // Named instances - have names
    let mut map = HashMap::new();
    map.insert("a".to_string(), UdpConfig::default());
    map.insert("b".to_string(), UdpConfig::default());
    let named = TransportInstances::Named(map);
    let items: Vec<_> = named.iter().collect();
    assert_eq!(items.len(), 2);
    // All named instances should have Some(name)
    assert!(items.iter().all(|(name, _)| name.is_some()));
}

#[test]
fn test_parse_peer_config() {
    let yaml = r#"
peers:
  - npub: "npub1abc123"
    alias: "gateway"
    addresses:
      - transport: udp
        addr: "192.168.1.1:2121"
        priority: 1
      - transport: tor
        addr: "xyz.onion:2121"
        priority: 2
    connect_policy: auto_connect
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();

    assert_eq!(config.peers.len(), 1);
    let peer = &config.peers[0];
    assert_eq!(peer.npub, "npub1abc123");
    assert_eq!(peer.alias, Some("gateway".to_string()));
    assert_eq!(peer.addresses.len(), 2);
    assert!(peer.is_auto_connect());

    // Check addresses are sorted by priority
    let sorted = peer.addresses_by_priority();
    assert_eq!(sorted[0].transport, "udp");
    assert_eq!(sorted[0].priority, 1);
    assert_eq!(sorted[1].transport, "tor");
    assert_eq!(sorted[1].priority, 2);
}

#[test]
fn test_parse_peer_minimal() {
    let yaml = r#"
peers:
  - npub: "npub1xyz"
    addresses:
      - transport: udp
        addr: "10.0.0.1:2121"
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();

    assert_eq!(config.peers.len(), 1);
    let peer = &config.peers[0];
    assert_eq!(peer.npub, "npub1xyz");
    assert!(peer.alias.is_none());
    // Default connect_policy is auto_connect
    assert!(peer.is_auto_connect());
    // Default priority is 100
    assert_eq!(peer.addresses[0].priority, 100);
}

#[test]
fn test_parse_multiple_peers() {
    let yaml = r#"
peers:
  - npub: "npub1peer1"
    addresses:
      - transport: udp
        addr: "10.0.0.1:2121"
  - npub: "npub1peer2"
    addresses:
      - transport: udp
        addr: "10.0.0.2:2121"
    connect_policy: on_demand
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();

    assert_eq!(config.peers.len(), 2);
    assert_eq!(config.auto_connect_peers().count(), 1);
}

#[test]
fn test_peer_config_builder() {
    let peer = PeerConfig::new("npub1test", "udp", "192.168.1.1:2121")
        .with_alias("test-peer")
        .with_address(PeerAddress::with_priority("tor", "xyz.onion:2121", 50));

    assert_eq!(peer.npub, "npub1test");
    assert_eq!(peer.alias, Some("test-peer".to_string()));
    assert_eq!(peer.addresses.len(), 2);
    assert!(peer.is_auto_connect());
}

#[test]
fn test_parse_nostr_discovery_config() {
    let yaml = r#"
node:
  discovery:
    nostr:
      enabled: true
      advertise: false
      policy: configured_only
      open_discovery_max_pending: 12
      app: "fips.nat.test.v1"
      signal_ttl_secs: 45
      advert_relays:
        - "wss://relay-a.example"
      dm_relays:
        - "wss://relay-b.example"
      stun_servers:
        - "stun:stun.example.org:3478"
peers:
  - npub: "npub1peer"
    addresses:
      - transport: udp
        addr: "nat"
"#;
    let config: Config = serde_yaml::from_str(yaml).unwrap();
    assert!(config.node.discovery.nostr.enabled);
    assert!(!config.node.discovery.nostr.advertise);
    assert_eq!(config.node.discovery.nostr.app, "fips.nat.test.v1");
    assert_eq!(config.node.discovery.nostr.signal_ttl_secs, 45);
    assert_eq!(
        config.node.discovery.nostr.policy,
        NostrDiscoveryPolicy::ConfiguredOnly
    );
    assert_eq!(config.node.discovery.nostr.open_discovery_max_pending, 12);
    assert_eq!(
        config.node.discovery.nostr.advert_relays,
        vec!["wss://relay-a.example".to_string()]
    );
    assert_eq!(
        config.node.discovery.nostr.dm_relays,
        vec!["wss://relay-b.example".to_string()]
    );
    assert_eq!(
        config.node.discovery.nostr.stun_servers,
        vec!["stun:stun.example.org:3478".to_string()]
    );
    assert_eq!(
        config.peers[0].addresses[0].addr, "nat",
        "udp:nat address should parse without special-casing in YAML"
    );
}

#[test]
fn test_validate_transport_advert_requires_nostr_enabled() {
    let mut config = Config::default();
    config.transports.udp = TransportInstances::Single(UdpConfig {
        advertise_on_nostr: Some(true),
        ..Default::default()
    });
    config.node.discovery.nostr.enabled = false;

    let err = config.validate().expect_err("validation should fail");
    assert!(err.to_string().contains("advertise_on_nostr"));

    config.transports.udp = TransportInstances::default();
    config.transports.webrtc = TransportInstances::Single(WebRtcConfig {
        advertise_on_nostr: Some(true),
        ..Default::default()
    });

    let err = config.validate().expect_err("validation should fail");
    assert!(err.to_string().contains("advertise_on_nostr"));
}

#[test]
fn test_validate_empty_peer_addresses_require_nostr_enabled() {
    let mut config = Config {
        peers: vec![PeerConfig {
            npub: "npub1peer".to_string(),
            ..Default::default()
        }],
        ..Default::default()
    };
    config.node.discovery.nostr.enabled = false;

    let err = config.validate().expect_err("validation should fail");
    assert!(err.to_string().contains("node.discovery.nostr"));
}

#[test]
fn test_validate_peer_addresses_optional_with_nostr_enabled() {
    // Empty addresses + Nostr discovery disabled -> error.
    let mut config = Config {
        peers: vec![PeerConfig {
            npub: "npub1peer".to_string(),
            ..Default::default()
        }],
        ..Default::default()
    };
    let err = config.validate().expect_err("validation should fail");
    assert!(err.to_string().contains("at least one address"));

    // Empty addresses + Nostr discovery enabled -> ok.
    config.node.discovery.nostr.enabled = true;
    config
        .validate()
        .expect("Nostr discovery should allow empty addresses");
}

#[test]
fn test_validate_nat_udp_advert_requires_relays_and_stun() {
    let mut config = Config::default();
    config.node.discovery.nostr.enabled = true;
    config.node.discovery.nostr.dm_relays.clear();
    config.transports.udp = TransportInstances::Single(UdpConfig {
        advertise_on_nostr: Some(true),
        public: Some(false),
        ..Default::default()
    });

    let err = config.validate().expect_err("validation should fail");
    assert!(err.to_string().contains("dm_relays"));

    config.node.discovery.nostr.dm_relays = vec!["wss://relay.example".to_string()];
    config.node.discovery.nostr.stun_servers.clear();
    let err = config.validate().expect_err("validation should fail");
    assert!(err.to_string().contains("stun_servers"));
}

#[test]
fn test_validate_webrtc_advert_requires_relays() {
    let mut config = Config::default();
    config.node.discovery.nostr.enabled = true;
    config.node.discovery.nostr.dm_relays.clear();
    config.transports.webrtc = TransportInstances::Single(WebRtcConfig {
        advertise_on_nostr: Some(true),
        ..Default::default()
    });

    let err = config.validate().expect_err("validation should fail");
    assert!(err.to_string().contains("dm_relays"));

    if let TransportInstances::Single(cfg) = &mut config.transports.webrtc {
        cfg.signal_relays = Some(vec!["wss://relay.example".to_string()]);
    }
    config
        .validate()
        .expect("WebRTC transport-specific relays should satisfy validation");
}

#[test]
fn test_is_loopback_addr_str() {
    assert!(is_loopback_addr_str("127.0.0.1:2121"));
    assert!(is_loopback_addr_str("127.0.0.5:9999"));
    assert!(is_loopback_addr_str("[::1]:2121"));
    assert!(is_loopback_addr_str("::1:2121"));
    assert!(is_loopback_addr_str("localhost:80"));
    assert!(!is_loopback_addr_str("0.0.0.0:2121"));
    assert!(!is_loopback_addr_str("192.168.1.1:2121"));
    assert!(!is_loopback_addr_str("[fd00::1]:2121"));
    assert!(!is_loopback_addr_str("core-vm.tail65015.ts.net:2121"));
    assert!(!is_loopback_addr_str("example.com:443"));
}

#[cfg(unix)]
#[test]
fn test_resolve_default_socket_call_sites_agree() {
    let _guard = ENV_MUTEX.lock().unwrap();

    let control_client = default_control_path().to_string_lossy().into_owned();
    let gateway_client = default_gateway_path().to_string_lossy().into_owned();
    let control_daemon = ControlConfig::default().socket_path;

    assert_eq!(control_daemon, control_client);

    let control_dir = Path::new(&control_client)
        .parent()
        .map(|p| p.to_string_lossy().into_owned())
        .unwrap_or_default();
    let gateway_dir = Path::new(&gateway_client)
        .parent()
        .map(|p| p.to_string_lossy().into_owned())
        .unwrap_or_default();
    assert_eq!(control_dir, gateway_dir);
}

#[cfg(unix)]
#[test]
fn test_resolve_default_socket_xdg_when_no_run_fips() {
    let _guard = ENV_MUTEX.lock().unwrap();

    let temp_dir = TempDir::new().unwrap();
    let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();

    // SAFETY: serialized by ENV_MUTEX, so no other test in this module
    // observes the transient process environment.
    unsafe {
        std::env::set_var("XDG_RUNTIME_DIR", temp_dir.path());
    }

    let path = resolve_default_socket("control.sock");

    // SAFETY: serialized by ENV_MUTEX.
    unsafe {
        match prev_xdg {
            Some(value) => std::env::set_var("XDG_RUNTIME_DIR", value),
            None => std::env::remove_var("XDG_RUNTIME_DIR"),
        }
    }

    assert!(
        path.starts_with("/run/fips/")
            || path.starts_with(&format!("{}/fips/", temp_dir.path().display())),
        "expected /run/fips or XDG path, got: {path}"
    );
}

#[cfg(unix)]
#[test]
fn test_resolve_default_socket_tmp_when_xdg_invalid() {
    let _guard = ENV_MUTEX.lock().unwrap();

    let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
    let bogus = "/nonexistent-xdg-runtime-dir-for-fips-test-zzz";

    // SAFETY: serialized by ENV_MUTEX.
    unsafe {
        std::env::set_var("XDG_RUNTIME_DIR", bogus);
    }

    let path = resolve_default_socket("gateway.sock");

    // SAFETY: serialized by ENV_MUTEX.
    unsafe {
        match prev_xdg {
            Some(value) => std::env::set_var("XDG_RUNTIME_DIR", value),
            None => std::env::remove_var("XDG_RUNTIME_DIR"),
        }
    }

    assert!(
        path.starts_with("/run/fips/") || path == "/tmp/fips-gateway.sock",
        "expected /run/fips or /tmp fallback, got: {path}"
    );
    assert!(
        !path.starts_with(bogus),
        "stale XDG_RUNTIME_DIR leaked into resolver: {path}"
    );
}

#[test]
fn test_validate_loopback_bind_with_external_peer_rejected() {
    use crate::config::PeerAddress;
    let mut config = Config::default();
    config.transports.udp = TransportInstances::Single(UdpConfig {
        bind_addr: Some("127.0.0.1:2121".to_string()),
        ..Default::default()
    });
    config.peers = vec![PeerConfig {
        npub: "npub1peer".to_string(),
        addresses: vec![PeerAddress::new("udp", "core-vm.tail65015.ts.net:2121")],
        ..Default::default()
    }];

    let err = config.validate().expect_err("validation should fail");
    let msg = err.to_string();
    assert!(msg.contains("loopback"), "got: {msg}");
    assert!(msg.contains("non-loopback"), "got: {msg}");
}

#[test]
fn test_validate_loopback_bind_with_loopback_peer_ok() {
    use crate::config::PeerAddress;
    let mut config = Config::default();
    config.transports.udp = TransportInstances::Single(UdpConfig {
        bind_addr: Some("127.0.0.1:2121".to_string()),
        ..Default::default()
    });
    config.peers = vec![PeerConfig {
        npub: "npub1peer".to_string(),
        addresses: vec![PeerAddress::new("udp", "127.0.0.2:2121")],
        ..Default::default()
    }];

    config
        .validate()
        .expect("loopback peer with loopback bind should validate");
}

#[test]
fn test_validate_outbound_only_exempt_from_loopback_check() {
    use crate::config::PeerAddress;
    let mut config = Config::default();
    // outbound_only overrides bind_addr → 0.0.0.0:0; the loopback
    // check must skip this transport entirely.
    config.transports.udp = TransportInstances::Single(UdpConfig {
        bind_addr: Some("127.0.0.1:2121".to_string()),
        outbound_only: Some(true),
        ..Default::default()
    });
    config.peers = vec![PeerConfig {
        npub: "npub1peer".to_string(),
        addresses: vec![PeerAddress::new("udp", "core-vm.tail65015.ts.net:2121")],
        ..Default::default()
    }];

    config
        .validate()
        .expect("outbound_only should be exempt from the loopback check");
}

#[test]
fn test_outbound_only_forces_ephemeral_bind() {
    let cfg = UdpConfig {
        bind_addr: Some("127.0.0.1:2121".to_string()),
        outbound_only: Some(true),
        ..Default::default()
    };
    assert_eq!(cfg.bind_addr(), "0.0.0.0:0");
    assert!(cfg.outbound_only());
}

#[test]
fn test_outbound_only_forces_advertise_off() {
    let cfg = UdpConfig {
        advertise_on_nostr: Some(true),
        outbound_only: Some(true),
        ..Default::default()
    };
    assert!(!cfg.advertise_on_nostr());
}

#[test]
fn test_udp_accept_connections_default_true() {
    let cfg = UdpConfig::default();
    assert!(cfg.accept_connections());
}
