package fr.umlv.seq;

import static java.util.stream.Collectors.toList;
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.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.time.Duration;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.UnaryOperator;
import java.util.stream.IntStream;

import org.junit.jupiter.api.Test;

@SuppressWarnings("static-method")
public class SeqTest {
  // Q1
  
  @Test
  public void testEmpty() {
    assertEquals(0, Seq.empty().size());
  }
  @Test
  public void testEmpty2() {
    var seq = Seq.empty();
    assertEquals(0, seq.size());
  }
  @Test
  public void testSize() {
    var seq = Seq.<String>empty();
    assertEquals(0, seq.size());
    seq = seq.append("foo");
    assertEquals(1, seq.size());
    seq = seq.append("bar");
    assertEquals(2, seq.size());
  }
  @Test
  public void testSize2() {
    var seq = Seq.<String>empty();
    Seq<String> seqFoo = seq.append("foo");
    assertEquals(0, seq.size());
    assertEquals(1, seqFoo.size());
  }
  @Test
  public void testAppendSimple() {
    var seq = Seq.<Integer>empty();
    seq = seq.append(3);
    assertEquals(1, seq.size());
  }
  @Test
  public void testAppendNull() {
    var seq = Seq.empty();
    assertThrows(NullPointerException.class, () -> seq.append(null));
  }
  
  // Q2
  @Test
  public void testAppendALot() {
    assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
      var seq = Seq.<Integer>empty();
      for(var i = 0; i < 1_000_000; i++) {
        seq = seq.append(i);
      }
      assertEquals(1_000_000, seq.size());
    });
  }
  @Test
  public void testAppendShared() {
    var seq = Seq.<Integer>empty();
    var seq2 = seq.append(2);
    var seq3 = seq.append(3);
    assertEquals(0, seq.size());
    assertEquals(1, seq2.size());
    assertEquals(1, seq3.size());
  }
  @Test
  public void testAppendShared2() {
    var seq = Seq.<Integer>empty();
    for(var i = 0; i < 4; i++) {
      seq = seq.append(i);
    }
    var seq2 = seq.append(-1);
    seq2 = seq2.append(-3);
    var seq3 = seq.append(-2);
    seq3 = seq3.append(-4);
    assertEquals(4, seq.size());
    assertEquals(6, seq2.size());
    assertEquals(6, seq3.size());
  }

  // Q3
  @Test
  public void testForEachEmpty() {
    var empty = Seq.empty();
    empty.forEach(x -> fail("should not be called"));
  }
  @Test
  public void testForEachSignature() {
    var seq = Seq.<String>empty();
    seq.forEach((Object o) -> fail("should not be called"));
  }
  @Test
  public void testForEachNonMutable() {
    var seq = Seq.<String>empty();
    var seqFoo = seq.append("foo");
    var seqBar = seq.append("bar");
    
    var list1 = new ArrayList<String>();
    seqFoo.forEach(list1::add);
    assertEquals(List.of("foo"), list1);
    var list2 = new ArrayList<String>();
    seqBar.forEach(list2::add);
    assertEquals(List.of("bar"), list2);
  }
  @Test
  public void testForEachNull() {
    var seq = Seq.empty();
    assertThrows(NullPointerException.class, () -> seq.forEach(null));
  }
  @Test
  public void testForEachNull2() {
    var seq = Seq.empty().append("foo");
    assertThrows(NullPointerException.class, () -> seq.forEach(null));
  }
  @Test
  public void testForEachALot() {
    var seq = Seq.<Integer>empty();
    for(var i = 0; i < 10_000; i++) {
      seq = seq.append(i);
    }
    var l = new ArrayList<Integer>();
    seq.forEach(l::add);
    assertEquals(IntStream.range(0, 10_000).boxed().collect(toList()), l);
  }
  @Test
  public void testForEachShared2() {
    var seq = Seq.<Integer>empty();
    for(var i = 0; i < 4; i++) {
      seq = seq.append(i);
    }
    var seq2 = seq.append(-1);
    seq2 = seq2.append(-3);
    var seq3 = seq.append(-2);
    seq3 = seq3.append(-4);
    
    var list = new ArrayList<Integer>();
    seq2.forEach(list::add);
    assertEquals(List.of(0, 1, 2, 3, -1, -3), list);
    var list2 = new ArrayList<Integer>();
    seq3.forEach(list2::add);
    assertEquals(List.of(0, 1, 2, 3, -2, -4), list2);
  }

  //Q4
  @Test
  public void testToString() {
    var seq = Seq.<Integer>empty();
    seq = seq.append(8).append(5).append(3);
    assertEquals(seq.toString(), "<8, 5, 3>");
  }
  
  @Test
  public void testToStringOneElement() {
    var seq = Seq.<String>empty();
    seq = seq.append("hello");
    assertEquals(seq.toString(), "<hello>");
  }
  
  @Test
  public void testToStringEmpty() {
    var seq = Seq.<Integer>empty();
    assertEquals(seq.toString(), "<>");
  }
  
  // Q5
  @Test
  public void testMapSimple() {
    var seq = Seq.<String>empty();
    seq = seq.append("1").append("2");
    var seq2 = seq.map(Integer::parseInt);
    
    var list = new ArrayList<Integer>();
    seq2.forEach(list::add);
    assertEquals(List.of(1, 2), list);
  }
  @Test
  public void testMapNull() {
    var seq = Seq.empty();
    assertThrows(NullPointerException.class, () -> seq.map(null));
  }
  @Test
  public void testMapAndAppend() {
    var seq = Seq.<String>empty();
    seq = seq.append("1").append("2");
    var seq2 = seq.map(Integer::parseInt);
    seq2 = seq2.append(3);
    
    var list = new ArrayList<Integer>();
    seq2.forEach(list::add);
    assertEquals(List.of(1, 2, 3), list);
  }
  @Test
  public void testMapNotCalledTooEarly() {
    var seq = Seq.<Integer>empty();
    seq = seq.append(42).append(777);
    var seq2 = seq.map(x -> { fail("should not be called"); return null; });
    
    assertEquals(2, seq2.size());
  }
  @Test
  public void testMapCompose() {
    var seq = Seq.<String>empty();
    seq = seq.append("1").append("2").append("3");
    var seq2 = seq.map(Integer::parseInt);
    var seq3 = seq2.map(x -> x.toString());
    
    var list = new ArrayList<String>();
    seq3.forEach(list::add);
    assertEquals(List.of("1", "2", "3"), list);
  }
  @Test
  public void testMapNotCalledTooEarlyButCompose() {
    var seq = Seq.<Integer>empty();
    seq = seq.append(42).append(777);
    var seq2 = seq.map(x -> { fail("should not be called"); return null; });
    var seq3 = seq2.map(x -> { fail("should not be called"); return null; });
    
    assertEquals(2, seq3.size());
  }
  @Test
  public void testMapNoSideEffect() {
    var seq = Seq.<Integer>empty();
    seq = seq.append(100).append(200);
    var seq2 = seq.map(x -> x * 2);
    seq2 = seq2.append(99);
    
    var list = new ArrayList<Integer>();
    seq.forEach(list::add);
    assertEquals(List.of(100, 200), list);
    ArrayList<Integer> list2 = new ArrayList<>();
    seq2.forEach(list2::add);
    assertEquals(List.of(200, 400, 99), list2);
  }
  @Test
  public void testMapToString() {
    var seq = Seq.<Integer>empty().append(10).append(20);
    seq = seq.map(x -> 2 * x);
    assertEquals("<20, 40>", seq.toString());
  }
  @Test
  public void testMapSignature1() {
    var seq = Seq.<Integer>empty().append(11).append(75);
    UnaryOperator<Object> identity = x -> x;  
    Seq<Object> seq2 = seq.map(identity);
    var list = new ArrayList<>();
    seq2.forEach(list::add);
    assertEquals(List.of(11, 75), list);
  }
  @Test
  public void testMapSignature2() {
    var seq = Seq.<String>empty().append("foo").append("bar");
    UnaryOperator<String> identity = x -> x;  
    Seq<Object> seq2 = seq.map(identity);
    var list = new ArrayList<>();
    seq2.forEach(list::add);
    assertEquals(List.of("foo", "bar"), list);
  }

  // Q6
  @Test
  public void testFindFirst() {
    var seq = Seq.<String>empty();
    assertFalse(seq.findFirst().isPresent());
    assertEquals("hello", seq.append("hello").findFirst().orElseThrow());
    assertEquals("allo", seq.append("allo").append("hola").findFirst().orElseThrow());
  }
  @Test
  public void testFindFirstMap() {
    var seq = Seq.<String>empty();
    assertFalse(seq.findFirst().map(x -> x).isPresent());
    assertEquals("z", seq.append("zoorg").append("garouf").map(x -> "" + x.charAt(0)).findFirst().orElseThrow());
  }
  @Test
  public void testFindFirstShared() {
    var empty = Seq.<String>empty();
    var seq = empty.append("item");
    assertAll(
      () -> assertFalse(empty.findFirst().isPresent()),
      () -> assertTrue(seq.findFirst().isPresent()),
      () -> assertEquals("item", seq.findFirst().orElseThrow())
      );
  }
  

  // Q7
  @Test
  public void testAppendAll() {
    var seq = Seq.<Integer>empty();
    var seq2 = seq.append(10).appendAll(seq.append(4).append(11));
    
    ArrayList<Integer> list = new ArrayList<>();
    seq2.forEach(list::add);
    assertEquals(List.of(10, 4, 11), list);
  }
  @Test
  public void testAppendAllNull() {
    var empty = Seq.empty();
    assertThrows(NullPointerException.class, () -> empty.appendAll(null));
  }
  @Test
  public void testAppendAllEmpty() {
    var seq = Seq.empty().appendAll(Seq.empty());
    
    assertEquals(0, seq.size());
    seq.forEach(x -> fail("should not be called"));
  }
  @Test
  public void testAppendAllSome() {
    var empty = Seq.<String>empty();
    var seq = empty.append("foo");
    var seq2 = empty.append("1").append("2").append("3").append("4");
    var seq3 = seq.appendAll(seq2);
    var seq4 = empty.append("1").append("2").append("-3").append("-4");
    var seq5 = seq.appendAll(seq4);
    
    var list3 = new ArrayList<String>();
    seq3.forEach(list3::add);
    assertEquals(List.of("foo", "1", "2", "3", "4"), list3);
    var list5 = new ArrayList<String>();
    seq5.forEach(list5::add);
    assertEquals(List.of("foo", "1", "2", "-3", "-4"), list5);
  }
  @Test
  public void testAppendAllMap() {
    var seq = Seq.<String>empty();
    var seq2 = seq.append("foo").map(String::length);
    var seq3 = Seq.<Integer>empty();
    var seq4 = seq2.appendAll(seq3.append(0));
    
    var list = new ArrayList<Integer>();
    seq4.forEach(list::add);
    assertEquals(List.of(3, 0), list);
  }
  @Test
  public void testAppendAllMap2() {
    var seq = Seq.<Integer>empty();
    var seq2 = seq.append(3);

    var seq3 = Seq.<String>empty();
    var seq4 = seq2.appendAll(seq3.append("").map(String::length));
    
    var list = new ArrayList<Integer>();
    seq4.forEach(list::add);
    assertEquals(List.of(3, 0), list);
  }
  @Test
  public void testAppendAllSignature() {
    var seq = Seq.empty().append("hell");
    var seq2 = Seq.<Integer>empty();
    Seq<Object> seq3 = seq.appendAll(seq2.append(6311));
    
    var list = new ArrayList<>();
    seq3.forEach(list::add);
    assertEquals(List.of("hell", 6311), list);
  }

  // Q8
  @Test
  public void testOf() {
    var seq = Seq.of(4, 3, 2, 1);
    
    var list = new ArrayList<Integer>();
    seq.forEach(list::add);
    assertEquals(List.of(4, 3, 2, 1), list);
  }
  @Test
  public void testOf2() {
    var seq = Seq.of("4", "3", "2", "1", "0");
    
    var list = new ArrayList<String>();
    seq.forEach(list::add);
    assertEquals(List.of("4", "3", "2", "1", "0"), list);
  }
  @Test
  public void testOfNull() {
    assertThrows(NullPointerException.class, () -> Seq.of((Object)null));
  }
  @Test
  public void testOfNull2() {
    assertThrows(NullPointerException.class, () -> Seq.of((Object[])null));
  }
  @Test
  public void testOfEmpty() {
    var seq = Seq.<Integer>of();
    assertEquals(0, seq.size());
    seq.forEach(x -> fail("should not be called"));
  }
  @Test
  public void testOfShared() {
    var seq = Seq.of(1, 2, 3, 4);
    var seq2 = seq.append(10);
    var seq3 = seq.append(20);
    
    var list2 = new ArrayList<Integer>();
    seq2.forEach(list2::add);
    assertEquals(List.of(1, 2, 3, 4, 10), list2);
    var list3 = new ArrayList<Integer>();
    seq3.forEach(list3::add);
    assertEquals(List.of(1, 2, 3, 4, 20), list3);
  }
  @Test
  public void testOfMap() {
    var seq = Seq.of(1, 2, 3, 4, 5);
    var seq2 = seq.map(x -> 2 * x);
    
    var list = new ArrayList<Integer>();
    seq2.forEach(list::add);
    assertEquals(List.of(2, 4, 6, 8, 10), list);
  }
  @Test
  public void testOfMapEarlyEvaluated() {
    var seq = Seq.of(1, 2, 3, 4, 5, 6);
    var seq2 = seq.map(x -> { fail("should not be called"); return x; });
    assertEquals(6, seq2.size());
    var seq3 = seq2.map(x -> { fail("should not be called"); return x; });
    assertEquals(6, seq3.size());
  }
  
  
  // Q9
  @Test
  public void testIterator() {
    var seq = Seq.<String>empty();
    seq = seq.append("foo").append("bar");
    Iterator<String> it = seq.iterator();
    assertTrue(it.hasNext());
    assertEquals("foo", it.next());
    assertTrue(it.hasNext());
    assertEquals("bar", it.next());
    assertFalse(it.hasNext());
  }
  @Test
  public void testIteratorALot() {
    var seq = Seq.<Integer>empty();
    for(var i = 0; i < 10_000; i++) {
      seq = seq.append(i);
    }
    Iterator<Integer> it = seq.iterator();
    for(var i = 0; i < 10_000; i++) {
      IntStream.range(0, 17).forEach(x -> assertTrue(it.hasNext()));
      assertEquals(i, (int)it.next());
    }
    IntStream.range(0, 17).forEach(x -> assertFalse(it.hasNext()));
  }
  @Test
  public void testIteratorAtTheEnd() {
    var seq = Seq.<Integer>empty();
    seq = seq.append(67).append(89);
    Iterator<Integer> it = seq.iterator();
    assertEquals(67, (int)it.next());
    assertEquals(89, (int)it.next());
    assertThrows(NoSuchElementException.class, it::next);
  }
  @Test
  public void testIteratorMap() {
    var seq = Seq.<Integer>empty();
    seq = seq.append(13).append(666);
    seq = seq.map(x -> x / 2);
    
    var list = new ArrayList<Integer>();
    seq.iterator().forEachRemaining(list::add);
    assertEquals(List.of(6, 333), list);
  }
  @Test
  public void testIteratorRemove() {
    var empty = Seq.empty();
    assertThrows(UnsupportedOperationException.class, () -> empty.iterator().remove());
  }
  @Test
  public void testIteratorEnhancedFor() {
    var seq = Seq.<Integer>empty();
    seq = seq.append(25).append(52);
    
    var list = new ArrayList<Integer>();
    for(var value: seq) {
      list.add(value);
    }
    assertEquals(List.of(25, 52), list);
  }
}
