package fr.umlv.exam;

import static java.util.Map.entry;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
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.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.junit.Test;

@SuppressWarnings("static-method")
public class VariableLookupTest {
  // Q1
  
  @Test
  public void testCreateWithNoParent() {
    VariableLookup.create(null);
  }
  @Test
  public void testAddVariable() {
    VariableLookup lookup = VariableLookup.create(null);
    int a = lookup.addVariable("a");
    assertEquals(0, a);
  }
  @Test
  public void testAddSeveralVariables() {
    VariableLookup lookup = VariableLookup.create(null);
    assertEquals(0, lookup.addVariable("foo"));
    assertEquals(1, lookup.addVariable("bar"));
    assertEquals(2, lookup.addVariable("baz"));
  }
  @Test(expected = IllegalStateException.class)
  public void testAddVariableTwoVariableWithTheSameName() {
    VariableLookup lookup = VariableLookup.create(null);
    assertEquals(0, lookup.addVariable("bob"));
   lookup.addVariable("bob");
  }
  @Test(expected = NullPointerException.class)
  public void testAddVariableWithANullName() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.addVariable(null);
  }
  @Test
  public void testSize() {
    VariableLookup lookup = VariableLookup.create(null);
    assertEquals(0, lookup.size());
    assertEquals(0, lookup.addVariable("banana"));
    assertEquals(1, lookup.size());
  }
  @Test
  public void testFindAVariable() {
    VariableLookup lookup = VariableLookup.create(null);
    int a1 = lookup.addVariable("a1");
    int b1 = lookup.addVariable("b1");
    assertTrue(lookup.find("a1").isPresent());
    assertEquals(a1, lookup.find("a1").getAsInt());
    assertTrue(lookup.find("b1").isPresent());
    assertEquals(b1, lookup.find("b1").getAsInt());
  }
  @Test
  public void testFindNoVariable() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.find("john").ifPresent(__ -> fail());
  }
  @Test(expected = NullPointerException.class)
  public void testFindNullName() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.find(null);
  }
  @Test(timeout = 2_000)
  public void testFindWithALotOfVariables() {
    VariableLookup lookup = VariableLookup.create(null);
    for(int i = 0; i < 1_000_000; i++) {
      assertEquals(i, lookup.addVariable("" + i));
    }
    for(int i = 0; i < 1_000_000; i++) {
      assertEquals(i, lookup.find("" + i).getAsInt());
    }
  }
  
  
  // Q2

  @Test
  public void testCreateWithAnEmptyParent() {
    VariableLookup parent = VariableLookup.create(null);
    VariableLookup lookup = VariableLookup.create(parent);
    assertEquals(0, lookup.size());
  }
  @Test
  public void testCreateWithANonEmptyParent() {
    VariableLookup parent = VariableLookup.create(null);
    assertEquals(0, parent.addVariable("aVariable"));
    assertEquals(1, parent.size());
    VariableLookup lookup = VariableLookup.create(parent);
    assertEquals(1, lookup.size());
  }
  @Test
  public void testCreateAVariableInAChildDoNotPoluteTheParent() {
    VariableLookup parent = VariableLookup.create(null);
    assertEquals(0, parent.size());
    VariableLookup lookup = VariableLookup.create(parent);
    assertEquals(0, lookup.addVariable("banzai"));
    assertEquals(1, lookup.size());
    assertEquals(0, parent.size());
  }
  @Test
  public void testCreateAVariableWithTheSameNameAsInTheParentIsAllowed() {
    VariableLookup parent = VariableLookup.create(null);
    assertEquals(0, parent.addVariable("ok"));
    VariableLookup lookup = VariableLookup.create(parent);
    assertEquals(1, lookup.addVariable("ok"));
  }
  @Test
  public void testFindAVariableDefinedInTheParent() {
    VariableLookup parent = VariableLookup.create(null);
    assertEquals(0, parent.addVariable("blop"));
    assertEquals(1, parent.addVariable("booh"));
    VariableLookup lookup = VariableLookup.create(parent);
    assertEquals(1, lookup.find("booh").getAsInt());
  }
  @Test
  public void testFindTheClosestVariableIfDefinedInDifferentVariableLookup() {
    VariableLookup parent = VariableLookup.create(null);
    assertEquals(0, parent.addVariable("twice"));
    VariableLookup lookup = VariableLookup.create(parent);
    assertEquals(1, lookup.addVariable("twice"));
    assertEquals(1, lookup.find("twice").getAsInt());
  }
  @Test
  public void testFindWithALotOfLookups() {
    VariableLookup lookup = VariableLookup.create(null);
    for(int i = 0; i < 1_000_000; i++) {
      lookup = VariableLookup.create(lookup);
      assertEquals(i, lookup.addVariable("" + i));
    }
    assertEquals(0, lookup.find("0").getAsInt());
  }
  
  
  // Q3
  
  @Test
  public void testForEachWithOneLookup() {
    VariableLookup lookup = VariableLookup.create(null);
    assertEquals(0, lookup.addVariable("a"));
    assertEquals(1, lookup.addVariable("b"));
    String[] vars = new String[2]; 
    lookup.forEach((name, index) -> vars[index] = name);
    assertEquals(List.of("a", "b"), Arrays.asList(vars));
  }
  @Test
  public void testForEachWithAParentLookup() {
    VariableLookup parent = VariableLookup.create(null);
    assertEquals(0, parent.addVariable("r0"));
    assertEquals(1, parent.addVariable("r1"));
    VariableLookup lookup = VariableLookup.create(parent);
    assertEquals(2, lookup.addVariable("r0"));
    assertEquals(3, lookup.addVariable("r2"));
    String[] vars = new String[4]; 
    lookup.forEach((name, index) -> vars[index] = name);
    assertEquals(List.of("r0", "r1", "r0", "r2"), Arrays.asList(vars));
  }
  @Test
  public void testForEachContravariance() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.forEach((Object name, Object index) -> fail());
  }
  @Test
  public void testForEachWithAnEmptyLookup() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.forEach((name, index) -> fail());
  }
  @Test(expected = NullPointerException.class)
  public void testForEachWithANullConsumer() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.forEach(null);
  }
  @Test
  public void testForEachWithALotOfVarsAndLookups() {
    VariableLookup parent = VariableLookup.create(null);
    VariableLookup lookup = parent;
    int length = 1_000_000;
    for(int i = 0; i < length; i++) {
      if (i % 1_000 == 0) {
        lookup = VariableLookup.create(lookup);
      }
      lookup.addVariable("" + i); 
    }
    String[] vars = new String[length]; 
    lookup.forEach((name, index) -> vars[index] = name);
    IntStream.range(0, length).forEach(i -> assertEquals("" + i, vars[i]));
  }
  
  
  // Q4
  
  @Test(timeout = 2_000)
  public void testVerifiesThatForEachIsInInsertionOrderIfCreateWithInsertionOrder() {
    VariableLookup lookup = VariableLookup.createWithInsertionOrder(null);
    for(int i = 0; i < 1_000_000; i++) {
      lookup.addVariable("" + i);
    }
    int[] value = { 0 };
    lookup.forEach((name, index) -> {
      int id = value[0]++;
      assertEquals(id, (int)index);
      assertEquals("" + id, name);
    });
  }
  @Test(timeout = 2_000)
  public void testVerifiesThatForEachIsInInsertionOrderIfCreateWithInsertionOrderWithSeveralLookups() {
    VariableLookup lookup = VariableLookup.createWithInsertionOrder(null);
    List<VariableLookup>  lookups = Stream.iterate(lookup, VariableLookup::createWithInsertionOrder).limit(1_000).collect(toList());
    for(int i = 0; i < 1_000_000; i++) {
      lookups.get(i / 1000).addVariable("" + i);
    }
    int[] value = { 0 };
    lookup.forEach((name, index) -> {
      int id = value[0]++;
      assertEquals(id, (int)index);
      assertEquals("" + id, name);
    });
  }

  // Q5

  @Test
  public void testSizeIteratorWithAnEmptyLookup() {
    VariableLookup empty = VariableLookup.create(null);
    ArrayList<Integer> list = new ArrayList<>();
    empty.sizeIterator().forEachRemaining(value -> list.add(value));
    assertEquals(List.of(0), list);
  }
  @Test
  public void testSizeIteratorWithASingleLookup() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.addVariable("foo");
    lookup.addVariable("bar");
    ArrayList<Integer> list = new ArrayList<>();
    lookup.sizeIterator().forEachRemaining(value -> list.add(value));
    assertEquals(List.of(2), list);
  }
  @Test
  public void testSizeIteratorWithSeveralLookups() {
    VariableLookup parent = VariableLookup.create(null);
    VariableLookup lookup1 = VariableLookup.create(parent);
    lookup1.addVariable("foo");
    lookup1.addVariable("bar");
    VariableLookup lookup2 = VariableLookup.create(lookup1);
    lookup2.addVariable("baz");
    ArrayList<Integer> list = new ArrayList<>();
    lookup2.sizeIterator().forEachRemaining(value -> list.add(value));
    assertEquals(List.of(3, 2, 0), list);
  }
  @Test
  public void testSizeIteratorHasNextIdempotence() {
    VariableLookup parent = VariableLookup.create(null);
    parent.addVariable("1");
    VariableLookup lookup = VariableLookup.create(parent);
    lookup.addVariable("2");
    Iterator<Integer> it = lookup.sizeIterator();
    IntStream.range(0, 100).forEach(__ -> assertTrue(it.hasNext()));
    it.next();
    IntStream.range(0, 100).forEach(__ -> assertTrue(it.hasNext()));
    it.next();
    IntStream.range(0, 100).forEach(__ -> assertFalse(it.hasNext()));
  }
  @Test(expected = NoSuchElementException.class)
  public void testSizeIteratorNextWithAnEmptyLookup() {
    VariableLookup empty = VariableLookup.create(null);
    Iterator<Integer> it = empty.sizeIterator();
    it.next();
    it.next();
  }
  @Test(expected = UnsupportedOperationException.class)
  public void testSizeIteratorRemoveWithALookup() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.addVariable("thisIsNotAVariable");
    Iterator<Integer> it = lookup.sizeIterator();
    it.next();
    it.remove();
  }
  
  
  // Q6 et 7
  
  @Test
  public void testStreamWithNoParentAndNoVariable() {
    VariableLookup lookup = VariableLookup.create(null);
    assertFalse(lookup.stream().findFirst().isPresent());
  }
  @Test
  public void testStreamWithNoParentAndASingleVariable() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.addVariable("variable");
    assertEquals(entry("variable", 0), lookup.stream().findFirst().get());
  }
  @Test
  public void testStreamWithNoParentAndSeveralVariables() {
    VariableLookup lookup = VariableLookup.create(null);
    lookup.addVariable("r0");
    lookup.addVariable("r1");
    lookup.addVariable("r2");
    assertEquals(Map.of("r0", 0, "r1", 1, "r2", 2).entrySet(), lookup.stream().collect(toSet()));
  }
  @Test
  public void testStreamWithSeveralParents() {
    VariableLookup parent = VariableLookup.create(null);
    parent.addVariable("r0");
    VariableLookup lookup1 = VariableLookup.create(parent);
    lookup1.addVariable("r0");
    lookup1.addVariable("r1");
    VariableLookup lookup2 = VariableLookup.create(lookup1);
    lookup2.addVariable("r0");
    assertEquals(Set.of(entry("r0", 0), entry("r0", 1), entry("r1", 2), entry("r0", 3)), lookup2.stream().collect(toSet()));
  }
  @Test
  public void testStreamIsNotParallelByDefault() {
    VariableLookup lookup = VariableLookup.create(null);
    assertFalse(lookup.stream().isParallel());
  }
  
  @Test
  public void testStreamWithSeveralParentsSomeWithNoVariable() {
    VariableLookup parent = VariableLookup.create(null);
    parent.addVariable("r0");
    VariableLookup lookup1 = VariableLookup.create(parent);
    VariableLookup lookup2 = VariableLookup.create(lookup1);
    lookup2.addVariable("r0");
    assertEquals(Set.of(entry("r0", 0), entry("r0", 1)), lookup2.stream().collect(toSet()));
  }

  @Test
  public void testStreamWithSeveralParentsTheLastOneHasNoVariable() {
    VariableLookup parent = VariableLookup.create(null);
    VariableLookup lookup1 = VariableLookup.create(parent);
    lookup1.addVariable("r0");
    VariableLookup lookup2 = VariableLookup.create(lookup1);
    lookup2.addVariable("r0");
    assertEquals(Set.of(entry("r0", 0), entry("r0", 1)), lookup2.stream().collect(toSet()));
  }

  
  // Q8
  
  @Test
  public void testStreamUserDefinedFastEnough() {
    VariableLookup lookup = VariableLookup.create(null);
    for(int i = 0; i < 10_000_000; i++) {
      lookup = VariableLookup.create(lookup);
    }
    long start = System.nanoTime();
    assertEquals(0, lookup.stream().count());
    long end = System.nanoTime();
    assertTrue("too long !", (end - start) < 10_000_000);
  }
}
