package fr.umlv.graph;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.Test;

@SuppressWarnings("static-method")
public class NodeMapGraphTest {
  @Test(expected=IllegalArgumentException.class)
  public void testInvalidNodeCount() {
    new NodeMapGraph<>(-17);
  }
  
  @Test
  public void testGetWeightEmpty() {
    int nodeCount = 20;
    NodeMapGraph<Object> graph = new NodeMapGraph<>(nodeCount);
    for(int  i = 0; i < nodeCount; i++) {
      for(int j = 0; j < nodeCount; j++) {
        assertFalse(graph.getWeight(i, j).isPresent());
      } 
    }
  }
  
  @Test(expected=IllegalArgumentException.class)
  public void testHasEdgeValid1() {
    NodeMapGraph<Object> graph = new NodeMapGraph<>(5);
    graph.getWeight(-1, 3);
  }
  @Test(expected=IllegalArgumentException.class)
  public void testHasEdgeValid2() {
    NodeMapGraph<Object> graph = new NodeMapGraph<>(5);
    graph.getWeight(2, -1);
  }
  @Test(expected=IllegalArgumentException.class)
  public void testHasEdgeValid3() {
    NodeMapGraph<Object> graph = new NodeMapGraph<>(5);
    graph.getWeight(5, 2);
  }
  @Test(expected=IllegalArgumentException.class)
  public void testHasEdgeValid4() {
    NodeMapGraph<Object> graph = new NodeMapGraph<>(5);
    graph.getWeight(3, 5);
  }
  
  
  
  @Test
  public void testAddEdge() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(7);
    graph.addEdge(3, 4, 2);
    assertEquals(2, (int)graph.getWeight(3, 4).get());
    assertFalse(graph.getWeight(4, 3).isPresent());
  }
  
  @Test
  public void testAddEdgeTwice() {
    NodeMapGraph<String> graph = new NodeMapGraph<>(7);
    graph.addEdge(3, 4, "foo");
    graph.addEdge(3, 4, "bar");
    assertEquals("bar", graph.getWeight(3, 4).get());
  }
  
  @Test(expected=IllegalArgumentException.class)
  public void testAddEdgeValid1() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(5);
    graph.addEdge(-1, 3, 7);
  }
  @Test(expected=IllegalArgumentException.class)
  public void testAddEdgeValid2() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(5);
    graph.addEdge(2, -1, 8);
  }
  @Test(expected=IllegalArgumentException.class)
  public void testAddEdgeValid3() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(5);
    graph.addEdge(5, 2, 9);
  }
  @Test(expected=IllegalArgumentException.class)
  public void testAddEdgeValid4() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(5);
    graph.addEdge(3, 5, 10);
  }
  
  @Test
  public void testAddEdgeALot() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(17);
    ThreadLocalRandom random = ThreadLocalRandom.current();
    IntStream.range(0, 1000).forEach(index -> {
      int i = random.nextInt(17);
      int j = random.nextInt(17);
      int value = random.nextInt(10_000) - 5_000;
      graph.addEdge(i, j, value);
      assertEquals(value, (int)graph.getWeight(i, j).get());
    });
  }
  
  
  @Test
  public void testNoEdges() {
    NodeMapGraph<String> graph = new NodeMapGraph<>(17);
    graph.edges(0, (src, dst, weight) -> fail());
  }
  
  @Test(expected = NullPointerException.class)
  public void testEdgesNullConsumer() {
    new NodeMapGraph<String>(17).edges(0, null);
  }
  
  @Test
  public void testEdgesOneEdge() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(3);
    graph.addEdge(1, 0, 2);
    graph.edges(0, (src, dst, weight) -> {
      assertEquals(1, src);
      assertEquals(0, dst);
      assertEquals(2, (int)weight);
    });
  }
  
  @Test(timeout = 5000)
  public void testEdgesALot() {
    int nodeCount = 200;
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(nodeCount);
    for(int i = 0; i < nodeCount; i++) {
      for(int j = 0; j < nodeCount; j++) {
        graph.addEdge(i, j, i % 10 + j);
      } 
    }
    for(int i = 0; i < nodeCount; i++) {
      graph.edges(i, (src, dst, weight) -> {
        assertEquals(src % 10 + dst, (int)weight);
      });
    }
  }
  
  @Test
  public void testNeighborsEmptyHasNext() {
    NodeMapGraph<Object> graph = new NodeMapGraph<>(6);
    Iterator<Integer> iterator = graph.neighborIterator(0);
    assertFalse(iterator.hasNext());
    assertFalse(iterator.hasNext());
    assertFalse(iterator.hasNext());
  }
  
  @Test(expected=NoSuchElementException.class)
  public void testNeighborsEmptyNext() {
    NodeMapGraph<Object> graph = new NodeMapGraph<>(6);
    graph.neighborIterator(0).next();
  }
  
  @Test
  public void testNeighborsOneEdge() {
    NodeMapGraph<String> graph = new NodeMapGraph<>(6);
    graph.addEdge(1, 2, "hello");
    Iterator<Integer> iterator = graph.neighborIterator(1);
    assertTrue(iterator.hasNext());
    assertEquals(2, (int)iterator.next());
    assertFalse(iterator.hasNext());
    try {
      iterator.next();
      fail();
    } catch(NoSuchElementException e) {
      // ok !
    }
  }
  
  @Test
  public void testNeighborsNoHasNext() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(10);
    for(int i = 0; i < 10; i++) {
      graph.addEdge(5, i, -1);  
    }
    
    HashSet<Integer> result = new HashSet<>();
    HashSet<Integer> expected = new HashSet<>();
    Iterator<Integer> iterator = graph.neighborIterator(5);
    for(int i = 0; i < 10; i++) {
      expected.add(i);
      result.add(iterator.next());
    }
    assertEquals(expected, result);
    
    assertFalse(iterator.hasNext());
    try {
      iterator.next();
      fail();
    } catch(NoSuchElementException e) {
      // ok !
    }
  }
  
  @Test(timeout = 5000)
  public void testNeighborsNonDestructive() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(12);
    for(int i = 0; i < 12; i++) {
      graph.addEdge(5, i, 67);  
    }
    Iterator<Integer> neighbors = graph.neighborIterator(5);
    while(neighbors.hasNext()) {
      neighbors.next();
    }
    for(int i = 0; i < 12; i++) {
      assertEquals(67, (int)graph.getWeight(5, i).get());  
    }
  }
  
  @Test(timeout = 5000)
  public void testNeighborSeveralHasNext() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(14);
    graph.addEdge(3, 7, 2);
    graph.addEdge(3, 5, 3);
    graph.addEdge(7, 3, 4);
    Iterator<Integer> neighbors = graph.neighborIterator(3);
    assertTrue(neighbors.hasNext());
    int vertex1 = neighbors.next();
    for(int i = 0; i < 5; i++) {
      assertTrue(neighbors.hasNext());
    }
    int vertex2 = neighbors.next();
    assertFalse(neighbors.hasNext());
    assertTrue((vertex1 == 5 && vertex2 == 7) ||
               (vertex1 == 7 && vertex2 == 5));
  }
  
  @Test
  public void testIteratorRemove() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(11);
    graph.addEdge(3, 10, 13);
    Iterator<Integer> neighbors = graph.neighborIterator(3);
    assertEquals(10, (int)neighbors.next());
    neighbors.remove();
    assertFalse(graph.getWeight(3, 10).isPresent());
  }
  
  @Test(expected = IllegalStateException.class)
  public void testIteratorRemoveInvalid() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(21);
    graph.addEdge(20, 19, 20);
    Iterator<Integer> neighbors = graph.neighborIterator(20);
    neighbors.remove();
  }
  
  @Test
  public void testIteratorRemoveTwiceInvalid() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(21);
    graph.addEdge(20, 19, 20);
    Iterator<Integer> neighbors = graph.neighborIterator(20);
    neighbors.next();
    neighbors.remove();
    assertFalse(graph.getWeight(20, 19).isPresent());
    try {
      neighbors.remove();
      fail();
    } catch(IllegalStateException e) {
      // ok !
    }
  }
  
  @Test
  public void testIteratorRemoveALot() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(50);
    for(int i = 0; i < 50; i++) {
      for(int j = 0; j < i; j++) {
        graph.addEdge(i, j, i + j);
      } 
    }
    
    for(int i = 0; i < 50; i++) {
      Iterator<Integer> neighbors = graph.neighborIterator(i);
      for(int j = 0; j < i; j++) {
        assertTrue(neighbors.hasNext());
        neighbors.next();
        neighbors.remove();
      }
      assertFalse(neighbors.hasNext());
    }
    
    for(int i = 0; i < 50; i++) {
      for(int j = 0; j < 50; j++) {
        assertFalse(graph.getWeight(i, j).isPresent());
      }
    }
  }
  
  
  @Test
  public void testNeighborStream() {
    NodeMapGraph<String> graph = new NodeMapGraph<>(17);
    assertEquals(graph.neighborStream(0).count(), 0);
  }
  
  @Test
  public void testNeighborStreamOneEdge() {
    NodeMapGraph<Integer> graph = new NodeMapGraph<>(3);
    graph.addEdge(1, 2, 3);
    assertEquals(graph.neighborStream(1).boxed().collect(Collectors.toSet()), Collections.singleton(2));
  }
  
  @Test(timeout = 5000)
  public void testNeighborStreamALot() {
    int nodeCount = 200;
    NodeMapGraph<Boolean> graph = new NodeMapGraph<>(nodeCount);
    for(int i = 0; i < nodeCount; i++) {
      for(int j = 0; j < nodeCount; j++) {
        graph.addEdge(i, j, true);
      } 
    }
    for(int i = 0; i < nodeCount; i++) {
      assertEquals(nodeCount, graph.neighborStream(i).distinct().count());
    }
  }
}
