package fr.umlv.irig.exam;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

import org.junit.Test;

@SuppressWarnings("static-method")
public class MultiMapTest {
  // Question 1
  
  @Test
  public void testCreate() {
    MultiMap<Object, Object> multiMap = MultiMap.create();
  }
  @Test
  public void testCreate2() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
  }
  
  @Test
  public void testSignatures() {
    class Check {
      void testSignature(Class<?> type) {
        if (type.isPrimitive() || type.isInterface() || type==Object.class) {
          return;
        }
        if (type.isArray()) {
          testSignature(type.getComponentType());
          return;
        }
        throw new AssertionError("invalid type in signature " + type);
      }
    }
    
    Arrays.stream(MultiMap.class.getMethods())
      .flatMap(m -> Stream.of(Stream.of(m.getReturnType()), Arrays.stream(m.getParameterTypes())).flatMap(s -> s))
      .forEach(new Check()::testSignature);
  }
  
  @Test
  public void testAdd() {
    MultiMap<Integer, String> multiMap = MultiMap.create();
    multiMap.add(3, "foo");
  }
  @Test(expected = NullPointerException.class)
  public void testAddNPE() {
    MultiMap<String, String> multiMap = MultiMap.create();
    multiMap.add(null, "hello");
  }
  @Test(expected = NullPointerException.class)
  public void testAddNPE2() {
    MultiMap<String, String> multiMap = MultiMap.create();
    multiMap.add("hello", null);
  }

  @Test
  public void testSizeEmpty() {
    assertEquals(0, MultiMap.create().size());
  }
  @Test
  public void testSizeAddOne() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("glub", 4);
    assertEquals(1, multiMap.size());
    multiMap.add("zorg", 13);
    assertEquals(2, multiMap.size());
  }
  @Test
  public void testSizeAddSameKey() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("key", 89);
    assertEquals(1, multiMap.size());
    multiMap.add("key", -56);
    assertEquals(2, multiMap.size());
  }
  @Test
  public void testSizeAddSameKeySameValue() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("azerty", 78);
    assertEquals(1, multiMap.size());
    multiMap.add("azerty", 78);
    assertEquals(1, multiMap.size());
  }

  @Test
  public void testGetValuesForKey() {
    MultiMap<String, String> multiMap = MultiMap.create();
    multiMap.add("cat", "cathy");
    Set<String> valuesForCat = multiMap.getValuesForKey("cat");
    assertEquals(1, valuesForCat.size());
    assertEquals(Set.of("cathy"), valuesForCat);
  }
  @Test
  public void testGetValuesForKeyWithSeveralValues() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("dog", 4);
    multiMap.add("dog", 14);
    Set<Integer> valuesForDog = multiMap.getValuesForKey("dog");
    assertEquals(2, valuesForDog.size());
    assertEquals(Set.of(4, 14), valuesForDog);
  }
  @Test
  public void testGetValuesForKeyWithSeveralValuesInOrder() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    for(int i = 0; i < 10; i++) {
      multiMap.add("test", i);
    }
    Set<Integer> values = multiMap.getValuesForKey("test");
    assertEquals(values.size(), 10);
    assertEquals(List.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), new ArrayList<>(values));
  }
  @Test
  public void testGetValuesForUnknownKey() {
    MultiMap<String, String> multiMap = MultiMap.create();
    Set<String> values = multiMap.getValuesForKey(56);
    assertTrue(values.isEmpty());    
  }
  @Test(expected=UnsupportedOperationException.class)
  public void testGetValuesForKeyNonMutable() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("foo", 5);
    multiMap.add("foo", 78);
    Set<Integer> values = multiMap.getValuesForKey("foo");
    values.add(6);
  }
  @Test(expected=UnsupportedOperationException.class)
  public void testGetValuesForKeyNonMutable2() {
    MultiMap<String, String> multiMap = MultiMap.create();
    multiMap.add("boy", "a");
    multiMap.add("boy", "b");
    Set<String> values = multiMap.getValuesForKey("boy");
    values.remove("a");
  }
  @Test(expected=UnsupportedOperationException.class)
  public void testGetValuesForUnknownKeyNonMutable() {
    MultiMap<String, String> multiMap = MultiMap.create();
    Set<String> values = multiMap.getValuesForKey("goo");
    values.addAll(Set.of("try"));
  }
  
  
  // Question 2
  
  @Test
  public void testToString() {
    MultiMap<String, String> multiMap = MultiMap.create();
    multiMap.add("a", "foo");
    multiMap.add("b", "foo");
    multiMap.add("a", "bar");
    multiMap.add("c", "baz");
    assertEquals(multiMap.toString(), "{a: foo, a: bar, b: foo, c: baz}");
  }
  @Test
  public void testToString2() {
    MultiMap<String, String> multiMap = MultiMap.create();
    multiMap.add("d", "foo");
    multiMap.add("c", "foo");
    multiMap.add("b", "bar");
    multiMap.add("c", "baz");
    assertEquals(multiMap.toString(), "{d: foo, c: foo, c: baz, b: bar}");
  }
  @Test
  public void testToStringEmpty() {
    assertEquals(MultiMap.create().toString(), "{}");
  }

  
  // Question 3
  
  @Test
  public void testCreateEntries() {
    MultiMap<String, Integer> multiMap = MultiMap.create(Map.entry("foo", 3));
    assertEquals(1, multiMap.size());
    assertEquals(Set.of(3), multiMap.getValuesForKey("foo"));
    assertEquals(Set.of(), multiMap.getValuesForKey("bar"));
    assertEquals("{foo: 3}", multiMap.toString());
  }
  @Test
  public void testCreateEntries2() {
    MultiMap<String, Number> multiMap = MultiMap.create(
        Map.entry("foo", 3), Map.entry("bar", 4.0), Map.entry("foo", 5.0));
    assertEquals(3, multiMap.size());
    assertEquals(Set.of(3, 5.0), multiMap.getValuesForKey("foo"));
    assertEquals(Set.of(4.0), multiMap.getValuesForKey("bar"));
    assertEquals("{foo: 3, foo: 5.0, bar: 4.0}", multiMap.toString());
  }
  @Test
  public void testCreateEntries3() {
    MultiMap<CharSequence, CharSequence> multiMap = MultiMap.create(
        Map.<CharSequence, String>entry("a", "hello"), Map.<String, CharSequence>entry("a", "boy"));
    assertEquals(2, multiMap.size());
    assertEquals(Set.of(), multiMap.getValuesForKey("foo"));
    assertEquals(Set.of("hello", "boy"), multiMap.getValuesForKey("a"));
    assertEquals("{a: hello, a: boy}", multiMap.toString());
  }
  @Test(expected = NullPointerException.class)
  public void testCreateEntriesNull() {
    MultiMap.create((Map.Entry<?,?>[])null);
  }
  @Test(expected = NullPointerException.class)
  public void testCreateSeveralEntriesNull() {
    MultiMap.create(null, null);
  }
  @Test(expected = NullPointerException.class)
  public void testCreateSeveralEntriesKeyNull() {
    MultiMap.create(Map.entry(null, "value"));
  }
  @Test(expected = NullPointerException.class)
  public void testCreateSeveralEntriesValueNull() {
    MultiMap.create(Map.entry("key", null));
  }
  
  
  // Question 4
  
  @Test
  public void testForEachEmpty() {
    MultiMap.create().forEach((key, value) -> fail());
  }
  @Test
  public void testForEach() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("foo", 3);
    multiMap.add("zorro", 5);
    multiMap.add("planet", 6);
    multiMap.forEach((key, value) -> {
      assertTrue(key.length() == value);
    });
  }
  @Test
  public void testForEachOrder() {
    MultiMap<Integer, Integer> multiMap = MultiMap.create();
    for(int i = 0; i < 10; i++) {
      multiMap.add(i, i);
    }
    int[] counter = new int[1];
    multiMap.forEach((key, value) -> {
      assertEquals(counter[0], (int)key);
      assertEquals(counter[0], (int)value);
      counter[0]++;
    });
  }
  @Test
  public void testForEachMultiple() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("foo", 7);
    multiMap.add("bar", 3);
    multiMap.add("foo", 8);
    multiMap.add("baz", 9);
    StringBuilder builder = new StringBuilder();
    multiMap.forEach((key, value) -> {
      builder.append(key).append(value);
    });
    assertEquals("foo7foo8bar3baz9", builder.toString());
  }
  @Test(expected = NullPointerException.class)
  public void testForEachNull() {
    MultiMap.create().forEach(null);
  }
  @Test
  public void testForEachFun() {
    class Fun implements BiConsumer<CharSequence, Object> {
      public void accept(CharSequence seq, Object o) {
        assertEquals("foo", o);
      }
    }
    MultiMap<String, String> multiMap = MultiMap.create();
    multiMap.add("a", "foo");
    multiMap.add("b", "foo");
    StringBuilder builder = new StringBuilder();
    multiMap.forEach(new Fun());
  }
  
  
  // Question 5
  
  @Test
  public void testAddAll() {
    MultiMap<String, String> multiMap1 = MultiMap.create();
    multiMap1.add("foo", "f");
    multiMap1.add("foo", "g");
    multiMap1.add("bar", "h");
    multiMap1.add("baz", "i");
    MultiMap<String, String> multiMap2 = MultiMap.create();
    multiMap2.add("foo", "j");
    multiMap2.add("bar", "k");
    multiMap2.add("foo", "f");
    multiMap2.add("bar", "l");
    multiMap1.addAll(multiMap2);
    assertEquals(7, multiMap1.size());
    assertEquals(List.of("f", "g", "j"), new ArrayList<>(multiMap1.getValuesForKey("foo")));
    assertEquals("{foo: f, foo: g, foo: j, bar: h, bar: k, bar: l, baz: i}", multiMap1.toString());
  }
  @Test(expected = NullPointerException.class)
  public void testAddAllNull() {
    MultiMap.create().addAll(null); 
  }
  @Test
  public void testAddAllFun() {
    MultiMap<Object, Object> multiMap1 = MultiMap.create();
    multiMap1.add(3, "waaas");
    MultiMap<Integer, String> multiMap2 = MultiMap.create();
    multiMap2.add(3, "wiiis");
    multiMap1.addAll(multiMap2);
    assertEquals(2, multiMap1.size());
    assertEquals(List.of("waaas", "wiiis"), new ArrayList<>(multiMap1.getValuesForKey(3)));
  }
  
  
  // Question 6
  
  @Test
  public void testAllValues() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("a", 6);
    multiMap.add("b", 5);
    multiMap.add("f", 4);
    multiMap.add("b", 3);
    Collection<Integer> allValues = multiMap.allValues();
    assertEquals(List.of(6, 5, 3, 4), new ArrayList<>(allValues));
  }
  @Test
  public void testAllValuesEmpty() {
    assertTrue(MultiMap.create().allValues().isEmpty());
  }
  @Test
  public void testAllValuesTwice() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("a", 3);
    multiMap.add("b", 4);
    multiMap.add("f", 3);
    Collection<Integer> allValues = multiMap.allValues();
    assertEquals(List.of(3, 4, 3), new ArrayList<>(allValues));
  }
  @Test
  public void testAllValuesContains() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("olk", 7);
    multiMap.add("pol", 0);
    multiMap.add("olk", 14);
    Collection<Integer> allValues = multiMap.allValues();
    assertTrue(allValues.contains(7));
    assertTrue(allValues.contains(0));
    assertTrue(allValues.contains(14));
  }
  @Test(expected = UnsupportedOperationException.class)
  public void testAllValuesNonDirectlyMutable() {
    Collection<Object> allValues = MultiMap.create().allValues();
    allValues.add("test");
  }
  @Test(expected = UnsupportedOperationException.class)
  public void testAllValuesNonDirectlyMutable2() {
    Collection<Object> allValues = MultiMap.create().allValues();
    allValues.addAll(List.of("test2"));
  }
  @Test
  public void testAllValuesMutationAfterCreation() {
    MultiMap<String, String> multiMap = MultiMap.create();
    Collection<String> allValues = multiMap.allValues();
    multiMap.add("foo", "bar");
    assertEquals(Set.of("bar"), new HashSet<>(allValues));
  }
  @Test
  public void testAllValuesIterator() {
    MultiMap<Integer, String> multiMap = MultiMap.create();
    multiMap.add(1, "pim");
    multiMap.add(3, "pam");
    multiMap.add(1, "poom");
    multiMap.add(1, "pum");
    Iterator<String> it = multiMap.allValues().iterator();
    assertTrue(it.hasNext());
    assertTrue(it.hasNext());
    assertEquals("pim", it.next());
    assertTrue(it.hasNext());
    assertEquals("poom", it.next());
    assertEquals("pum", it.next());
    assertTrue(it.hasNext());
    assertTrue(it.hasNext());
    assertEquals("pam", it.next());
    assertFalse(it.hasNext());
    assertFalse(it.hasNext());
  }
  @Test(expected = NoSuchElementException.class)
  public void testAllValuesEmptyIterator() {
    MultiMap.create().allValues().iterator().next();
  }
  @Test(expected = UnsupportedOperationException.class)
  public void testAllValuesIteratorRemove() {
    MultiMap<Integer, Integer> multiMap = MultiMap.create();
    multiMap.add(3, 4);
    multiMap.add(32, 24);
    multiMap.add(3, 16);
    Iterator<Integer> it = multiMap.allValues().iterator();
    assertEquals(4, (int)it.next());
    it.remove();
  }
  
  
  // Question 7
  
  @Test
  public void testAsMapOfLastValues() {
    MultiMap<String, Integer> multiMap = MultiMap.create();
    multiMap.add("for", 4);
    multiMap.add("if", 24);
    multiMap.add("while", 16);
    multiMap.add("if", 32);
    Map<String, Integer> map = multiMap.asMapOfLastValues();
    assertEquals(3, map.size());
    assertEquals(32, (int)map.get("if"));
    assertEquals(4, (int)map.get("for"));
    assertNull(map.get("loop"));
    assertEquals(23, (int)map.getOrDefault("default", 23));
    assertEquals(List.of("for", "if", "while"), new ArrayList<>(map.keySet()));
    assertEquals(List.of(4, 32, 16), new ArrayList<>(map.values()));
  }
  @Test
  public void testAsMapOfLastValuesEmpty() {
    MultiMap<Integer, String> multiMap = MultiMap.create();
    Map<Integer, String> map = multiMap.asMapOfLastValues();
    assertTrue(map.isEmpty());
    assertNull(map.get(3));
    assertNull(map.get("hello"));
  }
  @Test(expected = UnsupportedOperationException.class)
  public void testAsMapOfLastValuesNonMutable() {
    MultiMap<String, String> multiMap = MultiMap.create();
    Map<String, String> map = multiMap.asMapOfLastValues();
    map.put("foo", "bar");
  }
  @Test(expected = UnsupportedOperationException.class)
  public void testAsMapOfLastValuesNonMutableIterator() {
    MultiMap<String, String> multiMap = MultiMap.create();
    multiMap.add("ggg", "3");
    multiMap.add("ggg", "747");
    Map<String, String> map = multiMap.asMapOfLastValues();
    assertEquals(1, map.size());
    Iterator<Entry<String, String>> it = map.entrySet().iterator();
    assertTrue(it.hasNext());
    assertEquals(Map.entry("ggg", "747"), it.next());
    assertFalse(it.hasNext());
    it.remove();
  }
  @Test(timeout = 3_000)
  public void testAsMapOfLastValuesFast() {
    MultiMap<Integer, Integer> multiMap = MultiMap.create();
    Map<Integer, Integer> map = multiMap.asMapOfLastValues();
    for(int i = 0; i < 1_000_000; i++) {
      multiMap.add(i, i);
    }
    assertEquals(1_000_000, map.size());
    for(int i = 0; i < 1_000_000; i++) {
      assertTrue(map.containsKey(i));
      assertEquals(i, (int)map.get(i));
    }
  }
}
