package com.cube.gossip;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

import java.util.*;
import java.util.concurrent.*;

/**
 * Tests for Gossip Protocol
 */
public class GossipProtocolTest {
    
    private GossipProtocol gossip1;
    private GossipProtocol gossip2;
    private GossipProtocol gossip3;
    
    @BeforeEach
    public void setup() {
        GossipProtocol.GossipConfig config = GossipProtocol.GossipConfig.defaultConfig();
        
        gossip1 = new GossipProtocol("node-1", "localhost", 8080, config);
        gossip2 = new GossipProtocol("node-2", "localhost", 8081, 
            new GossipProtocol.GossipConfig(1000, 3, 5000, 15000, 3, 7947));
        gossip3 = new GossipProtocol("node-3", "localhost", 8082,
            new GossipProtocol.GossipConfig(1000, 3, 5000, 15000, 3, 7948));
    }
    
    @AfterEach
    public void teardown() {
        if (gossip1 != null) gossip1.shutdown();
        if (gossip2 != null) gossip2.shutdown();
        if (gossip3 != null) gossip3.shutdown();
    }
    
    @Test
    public void testSingleNodeStartup() {
        gossip1.start();
        
        // Should have local node
        Map<String, GossipProtocol.NodeState> state = gossip1.getClusterState();
        assertEquals(1, state.size());
        assertTrue(state.containsKey("node-1"));
        
        GossipProtocol.NodeState localNode = state.get("node-1");
        assertEquals(GossipProtocol.NodeState.Status.ALIVE, localNode.getStatus());
    }
    
    @Test
    public void testTwoNodeCluster() throws Exception {
        gossip1.start();
        gossip2.start();
        
        // Node 2 joins via node 1
        gossip2.join(Arrays.asList("localhost:7946"));
        
        // Wait for gossip to propagate
        Thread.sleep(3000);
        
        // Both nodes should see each other
        Map<String, GossipProtocol.NodeState> state1 = gossip1.getClusterState();
        Map<String, GossipProtocol.NodeState> state2 = gossip2.getClusterState();
        
        assertEquals(2, state1.size());
        assertEquals(2, state2.size());
        
        assertTrue(state1.containsKey("node-1"));
        assertTrue(state1.containsKey("node-2"));
        assertTrue(state2.containsKey("node-1"));
        assertTrue(state2.containsKey("node-2"));
    }
    
    @Test
    public void testThreeNodeCluster() throws Exception {
        gossip1.start();
        gossip2.start();
        gossip3.start();
        
        // Nodes join
        gossip2.join(Arrays.asList("localhost:7946"));
        Thread.sleep(1000);
        gossip3.join(Arrays.asList("localhost:7946", "localhost:7947"));
        
        // Wait for convergence
        Thread.sleep(5000);
        
        // All nodes should see all nodes
        assertEquals(3, gossip1.getClusterState().size());
        assertEquals(3, gossip2.getClusterState().size());
        assertEquals(3, gossip3.getClusterState().size());
    }
    
    @Test
    public void testAliveNodes() throws Exception {
        gossip1.start();
        gossip2.start();
        
        gossip2.join(Arrays.asList("localhost:7946"));
        Thread.sleep(3000);
        
        List<GossipProtocol.NodeState> aliveNodes1 = gossip1.getAliveNodes();
        List<GossipProtocol.NodeState> aliveNodes2 = gossip2.getAliveNodes();
        
        assertEquals(2, aliveNodes1.size());
        assertEquals(2, aliveNodes2.size());
    }
    
    @Test
    public void testEventListener() throws Exception {
        CountDownLatch joinLatch = new CountDownLatch(1);
        CountDownLatch aliveLatch = new CountDownLatch(1);
        
        gossip1.addListener(new GossipProtocol.GossipListener() {
            @Override
            public void onNodeJoined(GossipProtocol.NodeState node) {
                if (node.getNodeId().equals("node-2")) {
                    joinLatch.countDown();
                }
            }
            
            @Override
            public void onNodeLeft(GossipProtocol.NodeState node) {}
            
            @Override
            public void onNodeSuspected(GossipProtocol.NodeState node) {}
            
            @Override
            public void onNodeAlive(GossipProtocol.NodeState node) {
                if (node.getNodeId().equals("node-2")) {
                    aliveLatch.countDown();
                }
            }
            
            @Override
            public void onNodeDead(GossipProtocol.NodeState node) {}
        });
        
        gossip1.start();
        gossip2.start();
        gossip2.join(Arrays.asList("localhost:7946"));
        
        // Wait for join event
        assertTrue(joinLatch.await(5, TimeUnit.SECONDS));
        assertTrue(aliveLatch.await(5, TimeUnit.SECONDS));
    }
    
    @Test
    public void testStatistics() throws Exception {
        gossip1.start();
        gossip2.start();
        gossip3.start();
        
        gossip2.join(Arrays.asList("localhost:7946"));
        gossip3.join(Arrays.asList("localhost:7946"));
        Thread.sleep(3000);
        
        Map<String, Object> stats = gossip1.getStatistics();
        
        assertEquals("node-1", stats.get("localNodeId"));
        assertEquals(3, stats.get("totalNodes"));
        assertEquals(3L, stats.get("aliveNodes"));
        assertEquals(0L, stats.get("suspectedNodes"));
        assertEquals(0L, stats.get("deadNodes"));
    }
    
    @Test
    public void testGracefulLeave() throws Exception {
        gossip1.start();
        gossip2.start();
        
        gossip2.join(Arrays.asList("localhost:7946"));
        Thread.sleep(3000);
        
        assertEquals(2, gossip1.getClusterState().size());
        
        // Node 2 leaves gracefully
        gossip2.leave();
        gossip2.shutdown();
        
        // Wait for propagation
        Thread.sleep(3000);
        
        // Node 1 should still see node 2 but as leaving/dead
        GossipProtocol.NodeState node2State = gossip1.getClusterState().get("node-2");
        assertNotNull(node2State);
        assertTrue(node2State.getStatus() == GossipProtocol.NodeState.Status.LEAVING ||
                   node2State.getStatus() == GossipProtocol.NodeState.Status.DEAD);
    }
    
    @Test
    public void testHeartbeatIncrement() throws Exception {
        gossip1.start();
        
        GossipProtocol.NodeState localNode = gossip1.getClusterState().get("node-1");
        long initialHeartbeat = localNode.getHeartbeatCounter();
        
        // Wait for a few gossip rounds
        Thread.sleep(5000);
        
        long newHeartbeat = localNode.getHeartbeatCounter();
        assertTrue(newHeartbeat > initialHeartbeat);
    }
}
