use super::*;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};

fn test_addr(port: u16) -> SocketAddr {
    SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port)
}

fn test_node_info(n: u8) -> NodeInfo {
    let id = NodeId::from_bytes([n; 16]);
    NodeInfo::new(id, test_addr(9090 + n as u16), test_addr(8080 + n as u16))
}

#[test]
fn test_new_membership() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local.clone(), config);

    assert_eq!(membership.local_id(), local.id);
    assert_eq!(membership.member_count(), 0);
    assert!(!membership.is_joined());
}

#[test]
fn test_process_join_update() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local, config);

    let remote = test_node_info(2);
    let entry = MembershipEntry::new(remote.clone(), NodeState::Alive, 1);

    membership.process_membership_update(entry);

    assert_eq!(membership.member_count(), 1);
    assert!(membership.is_member(remote.id));
}

#[test]
fn test_suspect_and_fail_node() {
    let local = test_node_info(1);
    let config = SwimConfig::fast();
    let membership = SwimMembership::new(local, config);

    // Add a node
    let remote = test_node_info(2);
    let entry = MembershipEntry::new(remote.clone(), NodeState::Alive, 1);
    membership.process_membership_update(entry);

    // Suspect it
    membership.suspect_node(remote.id);

    let (_, state) = membership.all_members().into_iter().next().unwrap();
    assert_eq!(state, NodeState::Suspect);

    // Fail it
    membership.fail_node(remote.id);

    let (_, state) = membership.all_members().into_iter().next().unwrap();
    assert_eq!(state, NodeState::Dead);
}

#[test]
fn test_alive_node_refutes_suspicion() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local, config);

    // Add and suspect a node
    let mut remote = test_node_info(2);
    let entry = MembershipEntry::new(remote.clone(), NodeState::Alive, 1);
    membership.process_membership_update(entry);
    membership.suspect_node(remote.id);

    // Node refutes with higher incarnation
    remote.incarnation = 1;
    membership.alive_node(remote.id, remote.clone());

    let (_, state) = membership.all_members().into_iter().next().unwrap();
    assert_eq!(state, NodeState::Alive);
}

#[test]
fn test_select_probe_target() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local, config);

    // No members, no target
    assert!(membership.select_probe_target().is_none());

    // Add some members
    for i in 2..5 {
        let remote = test_node_info(i);
        let entry = MembershipEntry::new(remote, NodeState::Alive, 1);
        membership.process_membership_update(entry);
    }

    // Should select one of them
    let target = membership.select_probe_target();
    assert!(target.is_some());
}

#[test]
fn test_select_indirect_targets() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local, config);

    // Add some members
    for i in 2..10 {
        let remote = test_node_info(i);
        let entry = MembershipEntry::new(remote, NodeState::Alive, 1);
        membership.process_membership_update(entry);
    }

    let exclude = NodeId::from_bytes([2; 16]);
    let targets = membership.select_indirect_targets(exclude);

    // Should have up to indirect_probe_count targets
    assert!(!targets.is_empty());
    assert!(targets.len() <= membership.config.indirect_probe_count);

    // Should not include excluded node
    assert!(!targets.contains(&exclude));
}

#[test]
fn test_gossip_queue() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local, config);

    // Add nodes which queues gossip
    for i in 2..5 {
        let remote = test_node_info(i);
        membership.alive_node(remote.id, remote);
    }

    // Get gossip batch
    let batch = membership.get_gossip_batch(10);
    assert_eq!(batch.len(), 3);

    // Queue should be empty now
    let batch2 = membership.get_gossip_batch(10);
    assert!(batch2.is_empty());
}

#[test]
fn test_lamport_clock() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local, config);

    // Add member with high lamport time
    let remote = test_node_info(2);
    let entry = MembershipEntry::new(remote, NodeState::Alive, 100);
    membership.process_membership_update(entry);

    // Our clock should have advanced past 100
    let our_time = membership.lamport_clock.load(Ordering::SeqCst);
    assert!(our_time > 100);
}

#[tokio::test]
async fn test_join_no_seeds() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local, config);

    // Join with no seeds should succeed (bootstrap)
    membership.join(vec![]).await.unwrap();
    assert!(membership.is_joined());
}

#[tokio::test]
async fn test_leave() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local, config);

    membership.join(vec![]).await.unwrap();
    assert!(membership.is_joined());

    membership.leave().await.unwrap();
    assert!(!membership.is_joined());
}

#[test]
fn test_ignore_self_updates() {
    let local = test_node_info(1);
    let config = SwimConfig::default();
    let membership = SwimMembership::new(local.clone(), config);

    // Try to process update about ourselves
    let entry = MembershipEntry::new(local, NodeState::Dead, 999);
    membership.process_membership_update(entry);

    // Should not have added ourselves to members
    assert_eq!(membership.member_count(), 0);
}
