package fr.umlv.spined; 

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.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.IntStream;

import org.junit.Test;

@SuppressWarnings("static-method")
public class SpinalTapTest {
  // Q2
  @Test
  public void testOf() {
    assertEquals(List.of(1), SpinalTap.of(1).getValues());
    assertEquals(List.of(7, 13, 45), SpinalTap.of(7, 13, 45).getValues());
  }

  @Test(expected = NullPointerException.class)
  public void testOfNull() {
    SpinalTap.of((Object[])null);
  }
  @Test(expected = NullPointerException.class)
  public void testOfNull2() {
    SpinalTap.of(null, "foo");
  }
  @Test(expected = NullPointerException.class)
  public void testOfNull3() {
    SpinalTap.of("bar", null);
  }
  
  @Test
  public void testOfImmutable() {
    String[] array = new String[] { "foo" };
    SpinalTap<String> seq = SpinalTap.of(array);
    array[0] = "bar";
    assertEquals("foo", seq.getValues().get(0));
  }
  
  @Test
  public void testEmpty() {
    assertEquals(List.of(), SpinalTap.of().getValues());
  }
  
  @Test
  public void testOneConstructor() {
    Constructor<?>[] constructors = SpinalTap.class.getDeclaredConstructors();
    assertEquals(1, constructors.length);
    assertTrue(Modifier.isPrivate(constructors[0].getModifiers()));
  }
  
  @Test
  public void testPrepend() {
    SpinalTap<String> seq = SpinalTap.of("world");
    seq = seq.prepend("hello");
    assertEquals(List.of("hello"), seq.getValues());
  }
  
  @Test(expected = NullPointerException.class)
  public void testPrependNull() {
    SpinalTap.of("wizz").prepend((String[])null);
  }
  @Test(expected = NullPointerException.class)
  public void testPrependNull2() {
    SpinalTap.of("wazz").prepend("fuzz", null);
  }
  @Test(expected = NullPointerException.class)
  public void testPrependNull3() {
    SpinalTap.of("wuzz").prepend(null, "woosh");
  }
  
  @Test
  public void testPrependImmutable() {
    String[] array = new String[] { "foo" };
    SpinalTap<String> seq = SpinalTap.of("").prepend(array);
    array[0] = "bar";
    assertEquals("foo", seq.getValues().get(0));
  }
  
  // Q3
  @Test
  public void testForEach() {
    SpinalTap<String> seq = SpinalTap.of("a", "b", "c");
    ArrayList<String> list = new ArrayList<>();
    seq.forEach(list::add);
    assertEquals(List.of("a", "b", "c"), list);
  }
  
  @Test
  public void testForEach2() {
    SpinalTap<Integer> seq = SpinalTap.of(56, 98);
    seq = seq.prepend(-4, 34);
    ArrayList<Integer> list = new ArrayList<>();
    seq.forEach(list::add);
    assertEquals(List.of(-4, 34, 56, 98), list);
  }
  
  @Test
  public void testForEachEmpty() {
    SpinalTap.of().forEach(__ -> fail());
  }
  
  @Test
  public void testForEachContravariance() {
    Consumer<Object> consumer = __ -> {
      // empty
    };
    SpinalTap.of(1, 2, 3).forEach(consumer);
  }
  
  // Q4
  @Test
  public void testForEachBig() {
    SpinalTap<Integer> seq = SpinalTap.of();
    for(int i = 0; i < 1_000_000; i++) {
      seq = seq.prepend(i);
    }
    long[] sum = new long[1];
    seq.forEach(v -> sum[0] += v);
    assertEquals(1_000_000L * (1_000_000 - 1) / 2, sum[0]);
  }
  
  // Q5
  @Test
  public void testToString() {
    SpinalTap<Integer> seq = SpinalTap.of(1, 3, 6, 9);
    assertEquals("1 -> 3 -> 6 -> 9", seq.toString());
  }
  
  @Test
  public void testToString2() {
    SpinalTap<Integer> seq = SpinalTap.of(6, 9).prepend(1, 3);
    assertEquals("1 -> 3 -> 6 -> 9", seq.toString());
  }
  
  @Test
  public void testToStringEmpty() {
    SpinalTap<String> seq = SpinalTap.of();
    assertEquals("", seq.toString());
  }

  // Q6
  @Test
  public void testIterator() {
    SpinalTap<Integer> seq = SpinalTap.of(45, 78);
    Iterator<Integer> it = seq.iterator();
    assertTrue(it.hasNext());
    assertEquals(45, (int)it.next());
    assertTrue(it.hasNext());
    assertEquals(78, (int)it.next());
    assertFalse(it.hasNext());
  }

  @Test
  public void testIterator2() {
    SpinalTap<Integer> seq = SpinalTap.of(0, -1);
    Iterator<Integer> it = seq.iterator();
    assertTrue(it.hasNext());
    assertTrue(it.hasNext());
    assertEquals(0, (int)it.next());
    assertEquals(-1, (int)it.next());
    assertFalse(it.hasNext());
  }
  
  @Test
  public void testIterator3() {
    SpinalTap<String> seq = SpinalTap.of("+", "++").prepend("+++");
    Iterator<String> it = seq.iterator();
    assertTrue(it.hasNext());
    assertEquals("+++", it.next());
    assertTrue(it.hasNext());
    assertEquals("+", it.next());
    assertTrue(it.hasNext());
    assertEquals("++", it.next());
    assertFalse(it.hasNext());
  }
  
  @Test
  public void testIterator4() {
    SpinalTap<Integer> seq = SpinalTap.of(2, 3).prepend(0, 1);
    Iterator<Integer> it = seq.iterator();
    it.next();
    it.next();
    it.next();
    it.next();
    try {
      it.next();
      fail();
    } catch(NoSuchElementException e) {
      // ok
    }
  }
  
  @Test
  public void testIteratorEmpty() {
    SpinalTap<Double> seq = SpinalTap.of();
    Iterator<Double> it = seq.iterator();
    assertFalse(it.hasNext());
  }
  
  @Test
  public void testIteratorEmptyInMiddle() {
    SpinalTap<String> seq = SpinalTap.of("2").prepend().prepend("1");
    Iterator<String> it = seq.iterator();
    assertTrue(it.hasNext());
    assertEquals("1", it.next());
    assertTrue(it.hasNext());
    assertEquals("2", it.next());
    assertFalse(it.hasNext());
  }
  
  @Test(expected = NoSuchElementException.class)
  public void testIteratorEmpty2() {
    SpinalTap<Double> seq = SpinalTap.of();
    seq.iterator().next();
  }
  
  @Test(expected = UnsupportedOperationException.class)
  public void testIteratorRemove() {
    SpinalTap<Double> seq = SpinalTap.of(1.0);
    Iterator<Double> it = seq.iterator();
    it.next();
    it.remove();
  }

  @Test
  public void testIteratorBig() {
    SpinalTap<Integer> seq = SpinalTap.of();
    for(int i = 0; i < 1_000_000; i++) {
      seq = seq.prepend(i);
    }
    Iterator<Integer> it = seq.iterator();
    for(int i = 1_000_000; --i >= 0;) {
      assertEquals(i, (int)it.next());
    }
  }
  
  // Q7
  @Test
  public void testForeachLoop() {
    SpinalTap<Integer> seq = SpinalTap.of(1, 2, 5);
    ArrayList<Integer> list = new ArrayList<>();
    for(int value: seq) {
      list.add(value);
    }
    assertEquals(List.of(1, 2, 5), list);
  }
  
  // Q8
  @Test
  public void testMap() {
    SpinalTap<Integer> seq = SpinalTap.of(5, 8, 12);
    seq = seq.map(x -> x * 2);
    assertEquals(List.of(10, 16, 24), seq.getValues());
  }
  
  @Test
  public void testMap2() {
    SpinalTap<String> seq = SpinalTap.of("bar", "bazz");
    SpinalTap<Integer> seq2 = seq.map(String::length);
    seq2 = seq2.prepend(1, 2);
    ArrayList<Integer> list = new ArrayList<>();
    seq2.forEach(list::add);
    assertEquals(List.of(1, 2, 3, 4), list);
  }
  
  @Test
  public void testMap3() {
    SpinalTap<Integer> seq = SpinalTap.of(4, 5).prepend(1, 2, 3);
    SpinalTap<String> seq2 = seq.map(Object::toString);
    ArrayList<String> list = new ArrayList<>();
    seq2.forEach(list::add);
    assertEquals(List.of("1", "2", "3", "4", "5"), list);
  }
  
  @Test
  public void testMapMap() {
    SpinalTap<String> seq = SpinalTap.of("1357", "2468");
    SpinalTap<Integer> map = seq.map(Integer::parseInt);
    SpinalTap<String> map2 = map.map(Object::toString);
    ArrayList<String> list = new ArrayList<>();
    map2.forEach(list::add);
    assertEquals(List.of("1357", "2468"), list);
  }
  
  @Test
  public void testMapCovariance() {
    Function<String, String> fun = x -> x;
    SpinalTap<Object> seq = SpinalTap.of("foo", "bar").map(fun);
    assertEquals(List.of("foo", "bar"), seq.getValues());
  }
  
  @Test
  public void testMapCovariance2() {
    Function<Object, String> fun = Object::toString;
    SpinalTap<String> seq = SpinalTap.of("foo", "bar").map(fun);
    assertEquals(List.of("foo", "bar"), seq.getValues());
  }
  
  @Test
  public void testMapLazy() {
    SpinalTap<Integer> seq = SpinalTap.of(1, 0, 2, 4, 0);
    try {
      @SuppressWarnings("unused")
      SpinalTap<Integer> seq2 = seq.map(x -> 1 / x);
      // ok
    } catch (ArithmeticException e) {
      fail();
    }
  }
}
