package fr.umlv.bag;

import static java.util.Collections.shuffle;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.IntStream.range;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.sql.Timestamp;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@SuppressWarnings("static-method")
public class BagTest {
  // Q2
  
  @Test @Tag("Q2")
  public void shouldCreateABag() {
    Bag<Object> bag = Bag.create();
    assertNotNull(bag);
  }
  @Test  @Tag("Q2")
  public void shouldAddSeveralReturnTheRightCount() {
    var bag = Bag.<String>create();
    assertEquals(10, bag.add("splash", 10));
  }
  @Test  @Tag("Q2")
  public void shouldAddSeveralReturnTheRightCount2() {
    var bag = Bag.<String>create();
    assertEquals(5, bag.add("foo", 5));
    assertEquals(5, bag.count("foo"));
    assertEquals(42, bag.add("foo", 37));
    assertEquals(42, bag.count("foo"));
  }
  @Test  @Tag("Q2")
  public void shouldAddOneByOneAndReturnTheRightCount() {
    Bag<String> bag = Bag.create();
    assertAll(
        () -> assertEquals(1, bag.add("foo", 1)),
        () -> assertEquals(1, bag.add("bar", 1)),
        () -> assertEquals(2, bag.add("foo", 1))
    );
  }
  @Test  @Tag("Q2")
  public void shouldAddIdenticalElementsAndReturnTheRightCount() {
    var bag = Bag.<String>create();
    assertAll(
        () -> assertEquals(2, bag.add("blob", 2)),
        () -> assertEquals(3, bag.add("blob", 1))
    );
  }
  @Test  @Tag("Q2")
  public void shouldGetAnErrorWhenAddingZeroOrANegativeCount() {
    var bag = Bag.create();
    assertAll(
        () -> assertThrows(IllegalArgumentException.class, () -> bag.add("foo", 0)),
        () -> assertThrows(IllegalArgumentException.class, () -> bag.add("foo", -1))
    );
  }
  @Test @Tag("Q2")
  public void shouldGetAnErrorWhenAddingWithNegativeCountEvenIfTheTotalIsPositive() {
    var bag = Bag.create();
    bag.add("buzz", 1);
    assertThrows(IllegalArgumentException.class, () -> bag.add("buzz", -1));
  }

  @Test @Tag("Q2")
  public void shouldCountCorrecltyWheneverPresentOrNot() {
    var bag = Bag.<Integer>create();
    bag.add(1, 1);
    bag.add(1, 1);
    bag.add(42, 1);
    assertAll(
        () -> assertEquals(2, bag.count(1)),
        () -> assertEquals(1, bag.count(42)),
        () -> assertEquals(0, bag.count(153)),
        () -> assertEquals(0, bag.count("foo"))
    );
  }
  @Test @Tag("Q2")
  public void shouldGetAnErrorWhenAddingNullOrCountingNull() {
    var bag = Bag.create();
    assertAll(
        () -> assertThrows(NullPointerException.class, () -> bag.add(null, 1)),
        () -> assertThrows(NullPointerException.class, () -> bag.count(null))
    );
  }
  @Test @Tag("Q2")
  public void shouldNotExposeImplementation() throws NoSuchMethodException {
    var method = Bag.class.getMethod("create");
    assertEquals(Bag.class, method.getReturnType());
  }

  
  // Q3

  @Test @Tag("Q3")
  public void shouldIterateWithForEachOverAnElementWithMultipleOccurrence() {
    var bag = Bag.<Integer>create();
    bag.add(117, 3);
    bag.forEach(element -> assertEquals(117, element));
  }
  @Test @Tag("Q3")
  public void shouldIterateWithForEachOverAnElementWithMultipleOccurrence2() {
    var bag = Bag.<Integer>create();
    bag.add(117, 3);
    var counter = new Object() { int count; };
    bag.forEach(__ -> counter.count++);
    assertEquals(3, counter.count);
  }
  @Test @Tag("Q3")
  public void shouldIterateWithForEachOverDifferentValues() {
    var bag = Bag.<String>create();
    bag.add("foo", 3);
    bag.add("bar", 13);
    bag.add("foo", 4);
    var list = new ArrayList<String>();
    bag.forEach(list::add);
    var grouped = list.stream().collect(groupingBy(v -> v, Collectors.counting()));
    assertAll(
        () -> assertEquals(7L, grouped.get("foo")),
        () -> assertEquals(13L, grouped.get("bar"))
    );
  }
  @Test @Tag("Q3")
  public void shouldIterateWithForEachOverDifferentValues2() {
    var bag = Bag.<Integer>create();
    bag.add(34, 3);
    bag.add(48, 7);
    var set = new HashSet<Integer>();
    bag.forEach(set::add);
    assertEquals(Set.of(34, 48), set);
  }
  @Test @Tag("Q3")
  public void shouldIterateCompileCorrectly() {
    Bag<String> bag = Bag.create();
    bag.add("foo", 2);
    bag.forEach((Object o) -> assertEquals("foo", o));
  }
  @Test @Tag("Q3")
  public void shouldNotIterateWithForEachOverAnEmptyBag() {
    Bag<Object> empty = Bag.create();
    empty.forEach(__ -> fail("should not be called"));
  }
  @Test @Tag("Q3")
  public void shouldGetAnErrorWithForEachAndANull() {
    var bag = Bag.create();
    assertThrows(NullPointerException.class, () -> bag.forEach(null));
  }

  
  // Q4 & Q5

  @Test @Tag("Q4")
  public void shouldAnIteratorProperlyTyped() {
    var bag = Bag.<String>create();
    Iterator<String> it = bag.iterator();
    assertNotNull(it);
  }
  @Test @Tag("Q4")
  public void shouldAnIteratorProperlyTyped2() {
    var bag = Bag.<Integer>create();
    Iterator<Integer> it = bag.iterator();
    assertNotNull(it);
  }
  @Test @Tag("Q4")
  public void shouldIterateProperlyWithIteratorAndFailWhenAskedForNonExistentNext() {
    var bag = Bag.<String>create();
    bag.add("hello", 2);
    var iterator = bag.iterator();
    assertEquals("hello", iterator.next());
    assertEquals("hello", iterator.next());
    assertFalse(iterator.hasNext());
    assertThrows(NoSuchElementException.class, iterator::next);
  }
  @Test @Tag("Q4")
  public void shouldBeAbleToAskHasNextAsManyTimesAsYouWishWithoutMessingUpWithNext() {
    var bag = Bag.<String>create();
    bag.add("bob", 1);
    var iterator = bag.iterator();
    for (var i = 0; i < 100; i++) {
      assertTrue(iterator.hasNext());
    }
    assertEquals("bob", iterator.next());
    assertFalse(iterator.hasNext());
  }
  @Test @Tag("Q4")
  public void shouldBeAbleToGetAndUseTwoDifferentIterators() {
    var bag = Bag.<String>create();
    bag.add("bang", 1);

    var iterator1 = bag.iterator();
    assertTrue(iterator1.hasNext());
    assertEquals("bang", iterator1.next());
    assertFalse(iterator1.hasNext());

    var iterator2 = bag.iterator();
    assertTrue(iterator2.hasNext());
    assertEquals("bang", iterator2.next());
    assertFalse(iterator2.hasNext());
  }
  @Test @Tag("Q4")
  public void shouldHaveNoNextWithIteratorOverAnEmptyBag() {
    assertFalse(Bag.create().iterator().hasNext());
  }
  @Test @Tag("Q4")
  public void shouldGetAnErrorWhenAskingNextOnEmptyIterator() {
    var iterator = Bag.create().iterator();
    assertThrows(NoSuchElementException.class, iterator::next);
  }
  @Test @Tag("Q4")
  public void shouldGetAnErrorWhenTryingToRemoveWithoutCallingNextBefore() {
    var bag = Bag.<String>create();
    bag.add("foo", 3);
    var iterator = bag.iterator();
    iterator.next();
    assertThrows(UnsupportedOperationException.class, iterator::remove);
  }
  @Test @Tag("Q4")
  public void shouldIterateWithIteratorOverManyElements() {
    var bag = Bag.<Integer>create();
    var set = new HashSet<>();
    for (var i = 0; i < 1_000; i++) {
      bag.add(i, 1);
      set.add(i);
    }
    var set2 = new HashSet<>();
    var iterator = bag.iterator();
    for (var i = 0; i < 1_000; i++) {
      assertTrue(iterator.hasNext());
      set2.add(iterator.next());
    }
    assertFalse(iterator.hasNext());
    assertEquals(set, set2);
  }
  @Test @Tag("Q4")
  public void shouldIterateWithIteratorOverAFewElementsWithMultipleOccurrence() {
    var bag = Bag.<Integer>create();
    var set = new HashSet<Integer>();
    for (int i = 0; i < 1000; i++) {
      bag.add(i, 1);
      set.add(i);
    }
    var set2 = new HashSet<Integer>();
    var iterator = bag.iterator();
    while (iterator.hasNext()) {
      set2.add(iterator.next());
    }
    assertEquals(set, set2);
  }
  @Test @Tag("Q4")
  public void shouldIterateWithIteratorOverManyElementsWithoutDisturbingHasNext() {
    var bag = Bag.<String>create();
    for (var i = 0; i < 1_000; i++) {
      bag.add("" + i, 1);
    }
    var iterator = bag.iterator();
    assertTrue(iterator.hasNext());
    for (var i = 0; i < 1_000; i++) {
      if (i % 11 == 0) {
        for (var j = 0; j < 7; j++) {
          assertTrue(iterator.hasNext());
        }
      }
      iterator.next();
    }
    assertFalse(iterator.hasNext());
  }


  // Q6

  @Test @Tag("Q6")
  public void shouldBeAbleToDoAnEnhancedLoopOverABag() {
    var bag = Bag.<Integer>create();
    range(0, 100).forEach(element -> bag.add(element, 1));
    var set = range(0, 100).boxed().collect(toSet());
    var set2 = new HashSet<Integer>();
    for(var element: bag) {
      set2.add(element);
    }
    assertEquals(set, set2);
  }
  
  
  // Q7 and Q8

  @Test @Tag("Q7")
  public void shouldGetACollectionShouldHaveTheRightType() {
    var bag = Bag.<String>create();
    Collection<String> collection = bag.asCollection();
    assertNotNull(collection);
  }
  @Test @Tag("Q7")
  public void shouldGetACollectionWithExactlyOneElement() {
    var bag = Bag.<Integer>create();
    bag.add(4, 1);
    var collection = bag.asCollection();
    assertAll(
        () -> assertFalse(collection.isEmpty()),
        () -> assertEquals(1, collection.size()),
        () -> assertFalse(collection.isEmpty()),
        () -> assertTrue(collection.contains(4)),
        () -> assertFalse(collection.contains("hello"))
    );
  }
  @Test @Tag("Q7")
  public void shouldGetACollectionThatReflectModificationsAfterCreation() {
    var bag = Bag.<String>create();
    var collection = bag.asCollection();
    assertAll(
        () -> assertTrue(collection.isEmpty()),
        () -> assertEquals(0, collection.size()),
        () -> assertFalse(collection.contains("hello")),
        () -> assertFalse(collection.contains(42))
    );
    bag.add("hello", 2);
    assertAll(
        () -> assertFalse(collection.isEmpty()),
        () -> assertEquals(2, collection.size()),
        () -> assertTrue(collection.contains("hello")),
        () -> assertFalse(collection.contains(42))
    );
  }
  @Test @Tag("Q7")
  public void shouldNotBeAbleToModifyTheCollectionReturnedByAsCollection() {
    var collection = Bag.create().asCollection();
    assertThrows(UnsupportedOperationException.class, () -> collection.add("hello"));
  }
  @Test @Tag("Q7")
  public void shouldGetTheSameElementsTheRightNumberOfTimesWithAsCollection() {
    var bag = Bag.<Integer>create();
    bag.add(4, 1);
    bag.add(7, 2);
    bag.add(17, 5);
    bag.add(15, 3);
    var collection = bag.asCollection();
    assertEquals(Set.of(4, 7, 17, 15), new HashSet<>(collection));
    assertEquals(11, collection.size());
  }
  @Test @Tag("Q7")
  public void shouldGetAccessToTheCollectionAsAViewWithAsCollection() {
    var bag = Bag.<Integer>create();
    var collection = bag.asCollection();
    bag.add(69, 2);
    assertEquals(69, collection.iterator().next());
    assertEquals(69, collection.iterator().next());
  }
  @Test @Tag("Q7")
  public void shouldNotRecreateTheWholeBagWithAsCollection() {
    var bag = Bag.<Integer>create();
    range(0, 100_000).forEach(i -> bag.add(i, 1 + i));

    assertTimeoutPreemptively(Duration.ofMillis(1), () -> {
      assertEquals(100_001 * 100_000 / 2, bag.asCollection().size());  
    });
  }
  @Test @Tag("Q7")
  public void shouldBeEfficientWhenUsingContainsFromAsCollection() {
    var bag = Bag.<Integer>create();
    range(0, 100_000).forEach(i -> bag.add(i, 1 + i));

    assertTimeoutPreemptively(Duration.ofMillis(1_000), () -> {
      for (var i = 1; i < 100_000; i++) {
        assertFalse(bag.asCollection().contains(-i));
      }
    });
  }

  
  // Q9

  @Test @Tag("Q9")
  public void shouldGetTheElementsInInsertionOrderWithCreateOrderedByInsertionBag() {
    var bag = Bag.<String>createOrderedByInsertion();
    bag.add("foo", 2);
    bag.add("bar", 1);
    bag.add("foo", 1);
    var expected = List.of("foo", "foo", "foo", "bar");
    assertAll(
        () -> {
          var list = new ArrayList<String>();
          bag.forEach(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<String>();
          bag.iterator().forEachRemaining(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<>(bag.asCollection());
          assertEquals(expected, list);
        }
    );
  }
  @Test @Tag("Q9")
  public void shouldGetTheElementsInInsertionOrderWithALotOfElements() {
    var bag = Bag.<Integer>createOrderedByInsertion();
    range(0, 1_000_000).forEach(i -> bag.add(i, 1));
    var expected = range(0, 1_000_000).boxed().collect(toList());
    assertAll(
        () -> {
          var list = new ArrayList<Integer>();
          bag.forEach(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<Integer>();
          bag.iterator().forEachRemaining(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<>(bag.asCollection());
          assertEquals(expected, list);
        }
    );
  }
  @Test @Tag("Q9")
  public void shouldGetTheElementsInComparatorOrderWithCreateOrderedByComparator() {
    var bag = Bag.createOrderedByComparator(String::compareTo);
    bag.add("foo", 2);
    bag.add("bar", 1);
    bag.add("foo", 1);
    var expected = List.of("bar", "foo", "foo", "foo");
    assertAll(
        () -> {
          var list = new ArrayList<String>();
          bag.forEach(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<String>();
          bag.iterator().forEachRemaining(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<>(bag.asCollection());
          assertEquals(expected, list);
        }
    );
  }
  @Test @Tag("Q9")
  public void shouldBeAbleToCreateAnOrderedBagWithAComparatorDefinedOnASuperType() {
    Bag<String> bag = Bag.<String>createOrderedByComparator((Object o1, Object o2) -> o1.toString().compareTo(o2.toString()));
    bag.add("tomato", 2);
    bag.add("zoo", 1);
    bag.add("elephant", 1);
    var expected = List.of("elephant", "tomato", "tomato", "zoo");
    assertAll(
        () -> {
          var list = new ArrayList<String>();
          bag.forEach(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<String>();
          bag.iterator().forEachRemaining(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<>(bag.asCollection());
          assertEquals(expected, list);
        }
    );
  }
  @Test @Tag("Q9")
  public void shouldGetTheElementsInComparatorOrderWithALotOfElements() {
    var bag = Bag.createOrderedByComparator(Integer::compareTo);
    var expected = range(0, 1_000_000).boxed().collect(toList());
    var randomList = new ArrayList<>(expected);
    shuffle(randomList, new Random(0));
    randomList.forEach(i -> bag.add(i, 1));
    assertAll(
        () -> {
          var list = new ArrayList<Integer>();
          bag.forEach(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<Integer>();
          bag.iterator().forEachRemaining(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<>(bag.asCollection());
          assertEquals(expected, list);
        }
    );
  }
  @Test @Tag("Q9")
  public void shouldThrowWithCreateOrderedByElementBag() {
    assertThrows(NullPointerException.class, () -> Bag.createOrderedByComparator(null));
  }
  @Test @Tag("Q9")
  public void shouldNotExposeImplementation2() throws NoSuchMethodException {
    var method1 = Bag.class.getMethod("createOrderedByInsertion");
    assertEquals(Bag.class, method1.getReturnType());
    var method2 = Bag.class.getMethod("createOrderedByComparator", Comparator.class);
    assertEquals(Bag.class, method2.getReturnType());
  }

  
  // Q10


  @Test @Tag("Q10")
  public void shouldGetTheElementsInElementOrderWithCreateOrderedByElement() {
    var bag = Bag.<String>createOrderedByElement();
    bag.add("foo", 2);
    bag.add("bar", 1);
    bag.add("foo", 1);
    var expected = List.of("bar", "foo", "foo", "foo");
    assertAll(
        () -> {
          var list = new ArrayList<String>();
          bag.forEach(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<String>();
          bag.iterator().forEachRemaining(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<>(bag.asCollection());
          assertEquals(expected, list);
        }
    );
  }
  @Test @Tag("Q10")
  public void shouldCreateOrderedByElementWorksWithComparableOfSuperType() {
    Bag<Timestamp> bag = Bag.<Timestamp>createOrderedByElement();
    assertNotNull(bag);
  }
  @Test @Tag("Q10")
  public void shouldGetTheElementsInElementOrderWithALotOfElements() {
    var bag = Bag.<Integer>createOrderedByElement();
    range(0, 1_000_000).forEach(i -> bag.add(i, 1));
    var expected = range(0, 1_000_000).boxed().collect(toList());
    assertAll(
        () -> {
          var list = new ArrayList<Integer>();
          bag.forEach(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<Integer>();
          bag.iterator().forEachRemaining(list::add);
          assertEquals(expected, list);
        },
        () -> {
          var list = new ArrayList<>(bag.asCollection());
          assertEquals(expected, list);
        }
    );
  }
  @Test @Tag("Q10")
  public void shouldNotExposeImplementation3() throws NoSuchMethodException {
    var method = Bag.class.getMethod("createOrderedByElement");
    assertEquals(Bag.class, method.getReturnType());
  }
}
