package fr.umlv.bag;

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.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.NoSuchElementException;
import java.util.stream.IntStream;

import org.junit.Test;

@SuppressWarnings("static-method")
public class BagTest {
  @Test
  public void simpleBag() {
    Bag.createSimpleBag();
  }

  @Test
  public void add() {
    Bag<String> bag = Bag.createSimpleBag();
    assertEquals(1, bag.add("foo", 1));
    assertEquals(1, bag.add("bar", 1));
    assertEquals(2, bag.add("foo", 1));
  }

  @Test
  public void addSeveral() {
    Bag<String> bag = Bag.createSimpleBag();
    assertEquals(2, bag.add("blob", 2));
    assertEquals(3, bag.add("blob", 1));
  }

  @Test(expected = NullPointerException.class)
  public void addNull() {
    Bag.createSimpleBag().add(null, 1);
  }

  @Test(expected = IllegalArgumentException.class)
  public void addNegativeCount() {
    Bag.createSimpleBag().add("foo", -1);
  }

  @Test(expected = IllegalArgumentException.class)
  public void addZeroCount() {
    Bag.createSimpleBag().add("bar", 0);
  }

  @Test
  public void addNegativeCount2() {
    Bag<Object> bag = Bag.createSimpleBag();
    bag.add("foo", 1);
    try {
      bag.add("foo", -1);
      fail();
    } catch (IllegalArgumentException e) {
      // do nothing
    }
  }

  @Test
  public void count() {
    Bag<Integer> bag = Bag.createSimpleBag();
    bag.add(1, 1);
    bag.add(1, 1);
    bag.add(42, 1);
    assertEquals(2, bag.count(1));
    assertEquals(1, bag.count(42));
    assertEquals(0, bag.count(153));
    assertEquals(0, bag.count("foo"));
  }

  @Test(expected = NullPointerException.class)
  public void countNull() {
    Bag.createSimpleBag().count(null);
  }

  @Test
  public void addCount() {
    Bag<Object> bag = Bag.createSimpleBag();
    bag.add(1, 3);
    assertEquals(3, bag.count(1));
    bag.add("foobar", 7);
    assertEquals(7, bag.count("foobar"));
  }

  @Test
  public void forEach() {
    Bag<Integer> bag = Bag.createSimpleBag();
    bag.add(117, 3);
    bag.forEach(e -> assertEquals(117, (int) e));
  }

  @Test
  public void forEach2() {
    Bag<Integer> bag = Bag.createSimpleBag();
    bag.add(34, 1);
    bag.add(48, 1);
    HashSet<Integer> set = new HashSet<>();
    bag.forEach(set::add);
    assertEquals(Set.of(34, 48), set);
  }

  @Test
  public void forEachEmpty() {
    Bag<Object> empty = Bag.createSimpleBag();
    empty.forEach(__ -> fail());
  }

  @Test(expected = NullPointerException.class)
  public void forEachNull() {
    Bag.createSimpleBag().forEach(null);
  }

  @Test
  public void iterator() {
    Bag<String> bag = Bag.createSimpleBag();
    bag.add("hello", 2);
    Iterator<String> it = bag.iterator();
    assertEquals("hello", it.next());
    assertEquals("hello", it.next());
    try {
      it.next();
      fail();
    } catch (NoSuchElementException e) {
      // ok
    }
  }

  @Test
  public void iterator2() {
    Bag<String> bag = Bag.createSimpleBag();
    bag.add("bob", 1);
    Iterator<String> it = bag.iterator();
    for (int i = 0; i < 100; i++) {
      assertEquals(true, it.hasNext());
    }
    assertEquals("bob", it.next());
    assertFalse(it.hasNext());
  }

  @Test
  public void iteratorIterator() {
    Bag<String> bag = Bag.createSimpleBag();
    bag.add("bang", 1);

    Iterator<String> it1 = bag.iterator();
    assertTrue(it1.hasNext());
    assertEquals("bang", it1.next());
    assertFalse(it1.hasNext());

    Iterator<String> it2 = bag.iterator();
    assertTrue(it2.hasNext());
    assertEquals("bang", it2.next());
    assertFalse(it2.hasNext());
  }

  @Test
  public void iteratorHasNext() {
    assertFalse(Bag.createSimpleBag().iterator().hasNext());
  }

  @Test(expected = NoSuchElementException.class)
  public void iteratorNext() {
    Bag.createSimpleBag().iterator().next();
  }

  @Test(expected = UnsupportedOperationException.class)
  public void iteratorRemove() {
    Bag.createSimpleBag().iterator().remove();
  }

  @Test
  public void iteratorBig() {
    Bag<Integer> bag = Bag.createSimpleBag();
    HashSet<Integer> set = new HashSet<>();
    for (int i = 0; i < 1000; i++) {
      bag.add(i % 3, 1);
      set.add(i % 3);
    }
    HashSet<Integer> set2 = new HashSet<>();
    Iterator<Integer> it = bag.iterator();
    assertTrue(it.hasNext());
    for (; it.hasNext();) {
      set2.add(it.next());
    }
    assertEquals(set, set2);
    assertFalse(it.hasNext());
  }

  @Test
  public void iteratorBig2() {
    Bag<String> bag = Bag.createSimpleBag();
    for (int i = 0; i < 1000; i++) {
      bag.add(Integer.toString(i), 1);
    }
    Iterator<String> it = bag.iterator();
    assertTrue(it.hasNext());
    for (int i = 0; i < 1000; i++) {
      if (i % 11 == 0) {
        for (int j = 0; j < 7; j++) {
          it.hasNext();
        }
      }
      it.next();
    }
    assertFalse(it.hasNext());
  }

  @Test
  public void asCollection() {
    Bag<Integer> bag = Bag.createSimpleBag();
    bag.add(4, 1);
    assertEquals(1, bag.asCollection().size());
    assertFalse(bag.asCollection().isEmpty());
    assertTrue(bag.asCollection().contains(4));
    assertFalse(bag.asCollection().contains("hello"));
  }

  @Test(expected = UnsupportedOperationException.class)
  public void asCollectionAdd() {
    Bag.createSimpleBag().asCollection().add("hello");
  }

  @Test
  public void asCollectionSimple() {
    Bag<Integer> bag = Bag.createSimpleBag();
    bag.add(4, 1);
    bag.add(7, 2);
    bag.add(17, 1);
    bag.add(15, 1);
    // assertEquals(Set.of(4, 7, 7, 17, 15), new
    // HashSet<>(bag.asCollection()));
    assertEquals(Set.of(4, 7, 17, 15), new HashSet<>(bag.asCollection()));
  }

  @Test
  public void asCollectionView() {
    Bag<Integer> bag = Bag.createSimpleBag();
    Collection<Integer> collection = bag.asCollection();
    bag.add(69, 1);
    assertEquals(69, (int) collection.iterator().next());
  }

  @Test
  public void asCollectionOrderedByInsertion() {
    Bag<Integer> bag = Bag.createOrderedByInsertionBag();
    bag.add(4, 1);
    bag.add(7, 2);
    bag.add(17, 1);
    bag.add(15, 1);
    assertEquals(List.of(4, 7, 7, 17, 15), new ArrayList<>(bag.asCollection()));
  }

  @Test
  public void asCollectionOrderedByElement() {
    Bag<Integer> bag = Bag.createOrderedByElementBag(Integer::compare);
    bag.add(4, 1);
    bag.add(7, 2);
    bag.add(17, 1);
    bag.add(15, 1);
    assertEquals(List.of(4, 7, 7, 15, 17), new ArrayList<>(bag.asCollection()));
  }

  @Test(timeout = 1_000)
  public void asCollectionSize() {
    Bag<Integer> bag = Bag.createSimpleBag();
    IntStream.range(0, 100_000).forEach(i -> bag.add(i, 1 + i));
    assertEquals(100_001 * 100_000 / 2, bag.asCollection().size());
  }

  @Test(timeout = 3_000)
  public void asCollectionContains() {
    Bag<Integer> bag = Bag.createSimpleBag();
    IntStream.range(0, 100_000).forEach(i -> bag.add(i, 1 + 1));

    for (int i = 1; i < 100_000; i++) {
      assertFalse(bag.asCollection().contains(-i));
    }
  }

  @Test
  public void orderedByInsertionBag() {
    Bag<Integer> bag = Bag.createOrderedByInsertionBag();
    ArrayList<Integer> list = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
      bag.add(i, 1);
      list.add(i);
    }
    ArrayList<Integer> list2 = new ArrayList<>();
    bag.iterator().forEachRemaining(list2::add);
    assertEquals(list, list2);
  }

  @Test
  public void orderedByInsertionBag2() {
    Bag<Integer> bag = Bag.createOrderedByInsertionBag();
    ArrayList<Integer> list = new ArrayList<>();
    for (int j = 0; j < 5; j++) {
      for (int i = 0; i < 100; i++) {
        bag.add(i, 1);
      }
    }

    for (int i = 0; i < 100; i++) {
      for (int j = 0; j < 5; j++) {
        list.add(i);
      }
    }

    ArrayList<Integer> list2 = new ArrayList<>();
    bag.iterator().forEachRemaining(list2::add);
    assertEquals(list, list2);
  }

  @Test
  public void orderedByElementBagForEach() {
    Bag<Integer> bag = Bag.createOrderedByElementBag(Integer::compareTo);
    bag.add(31, 1);
    bag.add(16, 3);

    ArrayList<Integer> list = new ArrayList<>();
    bag.forEach(list::add);

    assertEquals(List.of(16, 16, 16, 31), list);
  }

  @Test
  public void orderedByElementBagIterator() {
    Bag<String> bag = Bag.createOrderedByElementBag(String::compareTo);
    bag.add("hello", 3);
    bag.add("boy", 2);
    bag.add("girl", 1);

    ArrayList<String> list = new ArrayList<>();
    bag.iterator().forEachRemaining(list::add);

    assertEquals(List.of("boy", "boy", "girl", "hello", "hello", "hello"), list);
  }

  @Test
  public void orderedByElementBagFromCollection() {
    List<String> list = List.of("foo", "zoom", "bar", "zoom");
    Bag<String> bag = Bag.createOrderedByElementBagFromCollection(list);

    ArrayList<String> list2 = new ArrayList<>();
    bag.iterator().forEachRemaining(list2::add);

    assertEquals(List.of("bar", "foo", "zoom", "zoom"), list2);
  }

  @Test
  public void orderedByElementBagFromCollection2() {
    List<Timestamp> list = List.of(new Timestamp(7), new Timestamp(3));
    Bag<Timestamp> bag = Bag.createOrderedByElementBagFromCollection(list);

    ArrayList<Timestamp> list2 = new ArrayList<>();
    bag.iterator().forEachRemaining(list2::add);

    assertEquals(List.of(new Timestamp(3), new Timestamp(7)), list2);
  }
}