package org.infinispan.util;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.infinispan.test.AbstractInfinispanTest;
import org.testng.annotations.Test;


/**
 * Tests functionality in {@link org.infinispan.util.DependencyGraph}.
 *
 * @author gustavonalle
 * @since 7.0
 */
@Test(testName = "util.DependencyGraphTest", groups = "unit")
public class DependencyGraphTest extends AbstractInfinispanTest {

   @Test
   public void testEmpty() throws CyclicDependencyException {
      assertTrue(new DependencyGraph().topologicalSort().isEmpty());
   }

   @Test
   public void testLinear() throws CyclicDependencyException {
      DependencyGraph<Integer> graph = new DependencyGraph<>();
      int size = 100;
      for (int i = 1; i <= size; i++) {
         graph.addDependency(i, i - 1);
      }
      List<Integer> sort = graph.topologicalSort();

      assertEquals(sort.size(), size + 1);
      assertEquals(sort.get(0), Integer.valueOf(100));
      assertEquals(sort.get(100), Integer.valueOf(0));
   }

   @Test
   public void testNonLinear() throws CyclicDependencyException {
      DependencyGraph<String> graph = new DependencyGraph<>();
      String A = "a";
      String B = "b";
      String C = "c";
      String D = "d";
      graph.addDependency(C, B);
      graph.addDependency(C, D);
      graph.addDependency(B, A);
      graph.addDependency(A, D);
      List<String> sort = graph.topologicalSort();

      assertEquals(sort, Arrays.asList(C, B, A, D));
   }

   @Test
   public void testIdempotency() throws CyclicDependencyException {
      DependencyGraph<String> g = new DependencyGraph<>();
      g.addDependency("N1", "N2");
      g.addDependency("N2", "N3");
      g.addDependency("N1", "N2");
      g.addDependency("N2", "N3");

      assertEquals(g.topologicalSort().size(), 3);
      assertEquals(g.topologicalSort(), Arrays.asList("N1", "N2", "N3"));
   }

   @Test
   public void testDependent() throws CyclicDependencyException {
      DependencyGraph<String> graph = new DependencyGraph<>();
      graph.addDependency("A", "B");
      graph.addDependency("A", "C");
      graph.addDependency("A", "D");
      graph.addDependency("D", "F");

      assertTrue(graph.hasDependent("B"));
      assertTrue(graph.hasDependent("C"));
      assertTrue(graph.hasDependent("D"));
      assertTrue(graph.hasDependent("F"));
      assertFalse(graph.hasDependent("A"));
      assertTrue(graph.getDependents("A").isEmpty());
      assertEquals(graph.getDependents("B").iterator().next(), "A");
      assertEquals(graph.getDependents("C").iterator().next(), "A");
      assertEquals(graph.getDependents("D").iterator().next(), "A");
      assertEquals(graph.getDependents("F").iterator().next(), "D");
   }

   @Test
   public void testConcurrentAccess() throws Exception {
      DependencyGraph<String> graph = new DependencyGraph<>();
      ExecutorService service = Executors.newCachedThreadPool(getTestThreadFactory("Worker"));
      CountDownLatch startLatch = new CountDownLatch(1);
      int threads = 20;
      ArrayList<Future<?>> futures = new ArrayList<>();
      for (int i = 0; i < threads; i++) {
         futures.add(submitTask("A", "B", startLatch, service, graph));
         futures.add(submitTask("A", "C", startLatch, service, graph));
         futures.add(submitTask("A", "D", startLatch, service, graph));
         futures.add(submitTask("A", "B", startLatch, service, graph));
         futures.add(submitTask("D", "B", startLatch, service, graph));
         futures.add(submitTask("D", "C", startLatch, service, graph));
         futures.add(submitTask("C", "B", startLatch, service, graph));
      }
      startLatch.countDown();
      awaitAll(futures);

      assertEquals(graph.topologicalSort(), Arrays.asList("A", "D", "C", "B"));
   }

   @Test
   public void testRemoveDependency() throws CyclicDependencyException {
      DependencyGraph<String> g = new DependencyGraph<>();
      g.addDependency("E", "B");
      g.addDependency("E", "C");
      g.addDependency("E", "D");
      g.addDependency("B", "D");
      g.addDependency("B", "C");
      g.addDependency("C", "D");

      assertEquals(g.topologicalSort(), Arrays.asList("E", "B", "C", "D"));

      g.removeDependency("E", "B");
      g.addDependency("B", "E");

      assertEquals(g.topologicalSort(), Arrays.asList("B", "E", "C", "D"));

      g.clearAll();

      assertTrue(g.topologicalSort().isEmpty());
   }

   @Test
   public void testRemoveElement() throws CyclicDependencyException {
      DependencyGraph<String> g = new DependencyGraph<>();
      g.addDependency("E", "B");
      g.addDependency("E", "C");
      g.addDependency("E", "D");
      g.addDependency("B", "D");
      g.addDependency("B", "C");
      g.addDependency("C", "D");

      assertEquals(g.topologicalSort(), Arrays.asList("E", "B", "C", "D"));

      g.remove("C");
      assertEquals(g.topologicalSort(), Arrays.asList("E", "B", "D"));

      g.remove("B");
      assertEquals(g.topologicalSort(), Arrays.asList("E", "D"));

      g.remove("E");
      assertEquals(g.topologicalSort(), Arrays.asList("D"));

      g.remove("D");
      assertTrue(g.topologicalSort().isEmpty());
   }

   @Test(expectedExceptions = IllegalArgumentException.class)
   public void testAddSelf() {
      new DependencyGraph<>().addDependency("N", "N");
   }

   @Test(expectedExceptions = IllegalArgumentException.class)
   public void testAdNull() {
      new DependencyGraph<>().addDependency("N", null);
   }

   @Test(expectedExceptions = CyclicDependencyException.class)
   public void testCycle() throws CyclicDependencyException {
      DependencyGraph<Object> graph = new DependencyGraph<>();
      Object o1 = new Object();
      Object o2 = new Object();
      Object o3 = new Object();
      graph.addDependency(o1, o2);
      graph.addDependency(o2, o3);
      graph.addDependency(o3, o1);
      graph.topologicalSort();
   }

   private Future<?> submitTask(final String from, final String to, final CountDownLatch waitingFor, ExecutorService onExecutor, final DependencyGraph<String> graph) {
      return onExecutor.submit(new Runnable() {
         @Override
         public void run() {
            try {
               waitingFor.await();
               graph.addDependency(from, to);
            } catch (InterruptedException ignored) {
            }
         }
      });
   }

   private void awaitAll(List<Future<?>> futures) throws Exception {
      for (Future f : futures) {
         f.get(10, TimeUnit.SECONDS);
      }
   }


}
