package org.quicktheories.core;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.quicktheories.impl.GenAssert.assertThatGenerator;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.quicktheories.generators.Generate;
import org.quicktheories.impl.ConcreteDetachedSource;
import org.quicktheories.impl.Constraint;
import org.quicktheories.impl.ExtendedRandomnessSource;

public class GenTest {
  
  ExtendedRandomnessSource source = Mockito.mock(ExtendedRandomnessSource.class);
  Gen<String> testee;
  
  @Before
  public void setup() {
    when(source.detach()).thenReturn(new ConcreteDetachedSource(source));
  }
  
  @Test
  public void limitsValuesByAssumptions() {
    Gen<Integer> ints = Sequence.of(1,2,3,4)
              .assuming(i -> i != 3);
    
    Stream<Integer> actual = generate(ints); 
    assertThat(actual.limit(3)).containsExactly(1,2,4);
  }  
  
  @Test
  public void reportsAssumptionFailure() {
    Gen<Integer> ints = Sequence.of(1,2,3,4)
              .assuming(i -> i != 3);
    
    generate(ints).limit(3).forEach(consume());
    verify(source).registerFailedAssumption(); 
  }  

  @Test
  public void reportsNoAssumptionFailureWhenNoneOccurs() {
    Gen<Integer> ints = Sequence.of(1,2,3,4);
    
    generate(ints).limit(3).forEach(consume());
    verify(source, never()).registerFailedAssumption(); 
  }  
  

  @Test
  public void mapsContentsWithFunction() {
    testee = Sequence.of(1,2,3,4,5)
              .map(i -> "" + (i + 1));
    
   Stream<String> actual = generate(testee); 
   assertThat(actual.limit(5)).containsExactly("2","3","4","5","6");
  }
  
  @Test
  public void mutatesContentsWithMod() {
    when(this.source.next(Constraint.none())).thenReturn(11L);
    Mod<String,String> m = (i,r) -> i + r.next(Constraint.none());
    testee = Sequence.of("1","2","3").mutate(m);
    Stream<String> actual = generate(testee);   
    assertThat(actual.limit(3)).containsExactly("111", "211","311");
  }
  
  @Test
  public void mapsContentsWithBiFunction() {
    testee = Sequence.of(1,2,3,4,5,6)
              .map( (a,b) -> "" + (a + b));
    
   Stream<String> actual = generate(testee); 
   assertThat(actual.limit(3)).containsExactly("3","7","11");
  }  
  
  @Test
  public void mapsContentsWithFunction3() {
    testee = Sequence.of(1,2,3,4,5,6,7,8,9)
              .map( (a,b,c) -> "" + (a + b + c));
    
   Stream<String> actual = generate(testee); 
   assertThat(actual.limit(3)).containsExactly("6","15","24");
  }  
  
  
  @Test
  public void flatMapsContents() {
    Gen<Integer> testee = Generate.pick(Arrays.asList(1,2)).flatMap(i -> Generate.range(0, i));
    assertThatGenerator(testee).generatesAllOf(0, 1, 2);
  }
  
  @Test
  public void describesValuesWithToStringByDefault() {
    Gen<Integer> ints = Sequence.of(1);
    assertThat(ints.asString(42)).isEqualTo("42");
  }
  
  @Test
  public void describesNullValuesAsNull() {
    Gen<Integer> ints = Sequence.of(1);
    assertThat(ints.asString(aNull())).isEqualTo("null");
  }
  
  @Test
  public void usesProvidedFunctionToDescribeValues() {
    Gen<Integer> ints = Sequence.of(1,2,3,4)
        .describedAs( i -> "about " + i);
    assertThat(ints.asString(42)).isEqualTo("about 42");
  }
  
  @Test
  public void preservesDescriptionWhenFiltering() {
    Gen<Integer> ints = Sequence.of(1,2,3,4)
        .describedAs( i -> "about " + i)
        .assuming( i -> i != 2);
    assertThat(ints.asString(42)).isEqualTo("about 42");
  }
  
  @Test
  public void willLooseDescriptionWhenMappingToSameType() {
    Gen<Integer> ints = Sequence.of(1,2,3,4)
        .describedAs( i -> "about " + i)
        .map( i -> i + 1);
    assertThat(ints.asString(42)).isEqualTo("42");
  }

  @Test
  public void combinesWithOtherGenUsingBiFunction() {
    Gen<Integer> as = Sequence.of(1,2,3,4);
    Gen<Integer> bs = Sequence.of(2,4,6,8);    
    
    Gen<Integer> combined = as.zip(bs, (a,b) -> a + b);
    
    Stream<Integer> actual = generate(combined).limit(4);
    assertThat(actual).containsExactly(3,6,9,12);
  }
  
  @Test
  public void combinesWithOtherGenUsingFunction3() {
    Gen<Integer> as = Sequence.of(1,2,3,4);
    Gen<Integer> bs = Sequence.of(2,4,6,8);   
    Gen<Integer> cs = Sequence.of(3,6,9,12);     
    
    Gen<Integer> combined = as.zip(bs, cs, (a,b,c) -> a + b + c);
    
    Stream<Integer> actual = generate(combined).limit(4);
    assertThat(actual).containsExactly(6,12,18,24);
  }  
  
  @Test
  public void combinesWithOtherGenUsingFunction4() {
    Gen<Integer> as = Sequence.of(1,2,3,4);
    Gen<Integer> bs = Sequence.of(2,4,6,8);   
    Gen<Integer> cs = Sequence.of(3,6,9,12);     
    Gen<Integer> ds = Sequence.of(1,3,5,7);       
    
    Gen<Integer> combined = as.zip(bs, cs, ds, (a,b,c,d) -> a + b + c + d);
    
    Stream<Integer> actual = generate(combined).limit(4);
    assertThat(actual).containsExactly(7,15,23,31);
  }
  
  @Test
  public void combinesWithOtherGenUsingFunction5() {
    Gen<Integer> as = Sequence.of(1,2,3,4);
    Gen<Integer> bs = Sequence.of(1,2,3,4); 
    Gen<Integer> cs = Sequence.of(1,2,3,4);     
    Gen<Integer> ds = Sequence.of(1,2,3,4);  
    Gen<Integer> es = Sequence.of(1,2,3,4);       
    
    Gen<Integer> combined = as.zip(bs, cs, ds, es, (a,b,c,d, e) -> a + b + c + d + e);
    
    Stream<Integer> actual = generate(combined).limit(4);
    assertThat(actual).containsExactly(5,10,15,20);
  }  
  
  @Test
  public void mixesValuesRandomlyWithOtherGens() {
    Gen<Integer> as = Sequence.of(1,2,3);
    Gen<Integer> bs = Sequence.of(2,4,6);  
    
    Gen<Integer> mixed = as.mix(bs);
    when(source.next(any(Constraint.class)))
    .thenReturn(0L, 99L, 0L);
    
    Stream<Integer> actual = generate(mixed).limit(3);
    
    assertThat(actual).containsExactly(2,1,4);
  }
  
  @SuppressWarnings("unchecked")
  @Test
  public void mapsToOptionalsWithoutEmptyWhenPercentageIs0() {
    Gen<Optional<Integer>> testee = Sequence.of(1,2,3).toOptionals(0);
    Stream<Optional<Integer>> actual = generate(testee).limit(3);
    assertThat(actual).containsExactly(Optional.of(1), Optional.of(2), Optional.of(3));
  }
  

  @SuppressWarnings("unchecked")
  @Test
  public void mapsToOnlyEmptyOptionalsWhenPercentageIs100() {
    Gen<Optional<Integer>> testee = Sequence.of(1,2,3).toOptionals(100);
    Stream<Optional<Integer>> actual = generate(testee).limit(3);
    assertThat(actual).containsExactly(Optional.empty(), Optional.empty(), Optional.empty());
  }  
  
  @Test
  public void includesSomeOptionalsWhenPercentageIs1() {
    Gen<Optional<Integer>> testee = Generate.constant(1).toOptionals(1);
    assertThatGenerator(testee).generatesAllOf(Optional.empty(), Optional.of(1));
  }

  @Test
  public void alwaysGeneratesRHSOfMixWhenWeightingIs100() {
    Gen<Integer> testee = Generate.constant(1).mix(Generate.constant(2), 100);
    assertThatGenerator(testee).generatesAllOf(2);
  }
  
  @Test
  public void alwaysGeneratesLHSOfMixWhenWeightingIs0() {
    Gen<Integer> testee = Generate.constant(1).mix(Generate.constant(2), 0);
    assertThatGenerator(testee).generatesAllOf(1);
  }  
  
  @Test
  public void producesValuesFromBothSidesWhenWeightingIs5050() {
    Gen<Integer> testee = Generate.constant(1).mix(Generate.constant(2), 50);
    assertThatGenerator(testee).generatesAllOf(1,2);
  }  
  
  private <T> Stream<T> generate(Gen<T> gen) {
    return Stream.generate( () -> gen.generate(source));
  }
  
  private <T> Consumer<T> consume() {
    return t -> {};
  }

  // dodge findbugs null check
  private Integer aNull() {
    return null;
  }
}


// deterministic Gen. Breaks contract so cannot be used outside
// of narrow scope of this test
class Sequence<T> implements Gen<T> {
  
  private final Iterator<T> sequence;
  
  Sequence(List<T> contents) {
    sequence = contents.iterator();
  }
  
  @SafeVarargs
  static <T> Sequence<T> of(T ...ts) {
    return new Sequence<>(Arrays.asList(ts));
  }

  @Override
  public T generate(RandomnessSource in) {
    return sequence.next();
  }
  
}