package fr.uge.gatherer;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.lang.reflect.AccessFlag;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Gatherer;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static java.util.function.Function.identity;
import static org.junit.jupiter.api.Assertions.*;

public class GathererDemoTest {
  @Nested
  public class Q1 {
    @Test
    public void filterIntegersEven() {
      var list = List.of(1, 2, 3, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.filterIntegers(x -> x % 2 == 0))
          .toList();
      assertEquals(List.of(2, 10), result);
    }

    @Test
    public void filterIntegersOdd() {
      var list = List.of(1, 2, 3, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.filterIntegers(x -> x % 2 == 1))
          .toList();
      assertEquals(List.of(1, 3, 7), result);
    }

    @Test
    public void filterIntegersAndCollectToSet() {
      var list = List.of(2, 4, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.filterIntegers(x -> x % 2 == 0))
          .collect(Collectors.toSet());
      assertEquals(Set.of(2, 4, 10), result);
    }

    @Test
    public void filterIntegersAll() {
      var list = List.of(1, 2, 3, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.filterIntegers(_ -> true))
          .toList();
      assertEquals(list, result);
    }

    @Test
    public void filterIntegersChainedWithMap() {
      var list = List.of(1, 2, 3, 4, 5);
      var result = list.stream()
          .gather(GathererDemo.filterIntegers(x -> x % 2 == 0))
          .map(x -> x * 2)
          .toList();
      assertEquals(List.of(4, 8), result);
    }

    @Test
    public void filterIntegersResultIsEmpty() {
      var list = List.of(1, 2, 3, 7, 10);
      var result = list.stream().gather(GathererDemo.filterIntegers(_ -> false)).toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void filterIntegersSingleElementMatch() {
      var list = List.of(3);
      var result = list.stream()
          .gather(GathererDemo.filterIntegers(x -> x % 2 == 1))
          .toList();
      assertEquals(List.of(3), result);
    }

    @Test
    public void filterIntegersEmptyStream() {
      var result = Stream.<Integer>empty()
          .gather(GathererDemo.filterIntegers(x -> x % 2 == 0))
          .toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void filterIntegersGathererAreIndependent() {
      var gatherer = GathererDemo.filterIntegers(x -> x % 2 == 0);
      var list = List.of(1, 2, 3, 4, 5);
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void filterIntegersPropagationWorks() {
      var list = List.of(1, 2, 4, 6, 8);
      var result = list.stream().gather(GathererDemo.filterIntegers(x -> x % 2 == 0)).limit(3).toList();
      assertEquals(List.of(2, 4, 6), result);
    }

    @Test
    public void filterIntegersIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.filterIntegers(x -> x % 2 == 0);
      assertNotSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void filterIntegersIntegratorIsGreedy() {
      var gatherer = GathererDemo.filterIntegers(x -> x % 2 == 0);
      assertInstanceOf(Gatherer.Integrator.Greedy.class, gatherer.integrator());
    }

    @Test
    public void filterIntegersPrecondition() {
      assertThrows(NullPointerException.class, () -> GathererDemo.filterIntegers(null));
    }

    @Test
    public void qualityOfImplementation() {
      var methods = Arrays.stream(GathererDemo.class.getMethods())
          .filter(m -> m.getName().equals("filterIntegers") && m.getParameterCount() == 1)
          .toList();
      assertFalse(methods.isEmpty());

      for (var method : methods) {
        assertNotEquals(Predicate.class, method.getParameters()[0].getType());
      }
    }
  }


  @Nested
  public class Q2 {
    @Test
    public void takeWhileIntegersEven() {
      var list = List.of(2, 1, 3, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhileIntegers(x -> x % 2 == 0))
          .toList();
      assertEquals(List.of(2), result);
    }

    @Test
    public void takeWhileIntegersOdd() {
      var list = List.of(1, 3, 2, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhileIntegers(x -> x % 2 == 1))
          .toList();
      assertEquals(List.of(1, 3), result);
    }

    @Test
    public void takeWhileIntegersAndCollectToSet() {
      var list = List.of(2, 4, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhileIntegers(x -> x % 2 == 0))
          .collect(Collectors.toSet());
      assertEquals(Set.of(2, 4), result);
    }

    @Test
    public void takeWhileIntegersAll() {
      var list = List.of(1, 2, 3, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhileIntegers(_ -> true))
          .toList();
      assertEquals(list, result);
    }

    @Test
    public void takeWhileIntegersNoneMatching() {
      var list = List.of(1, 2, 3, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhileIntegers(_ -> false))
          .toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void takeWhileIntegersSingleElementMatch() {
      var list = List.of(3);
      var result = list.stream()
          .gather(GathererDemo.takeWhileIntegers(x -> x % 2 == 1))
          .toList();
      assertEquals(List.of(3), result);
    }

    @Test
    public void takeWhileIntegersGathererAreIndependent() {
      var gatherer = GathererDemo.takeWhileIntegers(x -> x % 2 == 0);
      var list = List.of(2, 4, 5, 6, 7);
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void takeWhileIntegersPropagationWorks() {
      var list = List.of(2, 4, 6, 8, 1, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhileIntegers(x -> x % 2 == 0))
          .limit(3)
          .toList();
      assertEquals(List.of(2, 4, 6), result);
    }

    @Test
    public void takeWhileIntegersStopsProcessingAfterPredicateFails() {
      var list = List.of(2, 4, 3, 6, 8);
      var result = list.stream()
          .peek(x -> {
            if (x == 6) {
              Assertions.fail("Should not process the element 6");
            }
          })
          .gather(GathererDemo.takeWhileIntegers(x -> x % 2 == 0))
          .toList();
      assertEquals(List.of(2, 4), result);
    }

    @Test
    public void takeWhileIntegersIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.takeWhileIntegers(x -> x % 2 == 0);
      assertSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void takeWhileIntegersIntegratorIsNotGreedy() {
      var gatherer = GathererDemo.takeWhileIntegers(x -> x % 2 == 0);
      assertFalse(gatherer.integrator() instanceof Gatherer.Integrator.Greedy);
    }

    @Test
    public void takeWhileIntegersPrecondition() {
      assertThrows(NullPointerException.class, () -> GathererDemo.takeWhileIntegers(null));
    }

    @Test
    public void qualityOfImplementation() {
      var methods = Arrays.stream(GathererDemo.class.getMethods())
          .filter(m -> m.getName().equals("takeWhileIntegers") && m.getParameterCount() == 1)
          .toList();
      assertFalse(methods.isEmpty());

      for (var method : methods) {
        assertNotEquals(Predicate.class, method.getParameters()[0].getType());
      }
    }
  }


  @Nested
  public class Q3 {
    @Test
    public void takeWhileEven() {
      var list = List.of(2, 1, 3, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhile(x -> x % 2 == 0))
          .toList();
      assertEquals(List.of(2), result);
    }

    @Test
    public void takeWhileOdd() {
      var list = List.of(1, 3, 2, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhile(x -> x % 2 == 1))
          .toList();
      assertEquals(List.of(1, 3), result);
    }

    @Test
    public void takeWhileAndCollectToSet() {
      var list = List.of(2, 4, 7, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhile(x -> x % 2 == 0))
          .collect(Collectors.toSet());
      assertEquals(Set.of(2, 4), result);
    }

    @Test
    public void takeWhileAll() {
      var list = List.of("foo", "bar", "baz");
      var result = list.stream()
          .gather(GathererDemo.takeWhile(_ -> true))
          .toList();
      assertEquals(list, result);
    }

    @Test
    public void takeWhileWithDifferentDoubles() {
      var list = List.of(1.5, 2.5, -1.0, 3.5, 4.5, 5.5);
      var result = list.stream()
          .gather(GathererDemo.takeWhile(d -> d < 4.0))
          .toList();
      assertEquals(List.of(1.5, 2.5, -1.0, 3.5), result);
    }

    @Test
    public void takeWhileSingleElementMatch() {
      var list = List.of("foo");
      var result = list.stream()
          .gather(GathererDemo.takeWhile(Predicate.not(String::isEmpty)))
          .toList();
      assertEquals(List.of("foo"), result);
    }

    @Test
    public void takeWhileEmptyStream() {
      var result = Stream.empty()
          .gather(GathererDemo.takeWhile(_ -> true))
          .toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void takeWhileResultIsEmpty() {
      var list = List.of(1, "foo", true, 4.0);
      var result = list.stream().gather(GathererDemo.takeWhile(_ -> false)).toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void takeWhileGathererAreIndependent() {
      var gatherer = GathererDemo.takeWhileIntegers(x -> x % 2 == 0);
      var list = List.of(2, 4, 5, 6, 7);
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void takeWhilePropagationWorks() {
      var list = List.of(2, 4, 6, 8, 1, 10);
      var result = list.stream()
          .gather(GathererDemo.takeWhile(x -> x % 2 == 0))
          .limit(3)
          .toList();
      assertEquals(List.of(2, 4, 6), result);
    }

    @Test
    public void takeWhileStopsProcessingAfterPredicateFails() {
      var list = List.of("a", "bb", "ccc", "dddd", "eeeee");
      var result = list.stream()
          .peek(x -> {
            if (x.equals("dddd")) {
              fail("Should not processing element dddd");
            }
          })
          .gather(GathererDemo.takeWhile(s -> s.length() <= 2))
          .toList();
      assertEquals(List.of("a", "bb"), result);
    }

    @Test
    public void takeWhileIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.takeWhile(_ -> true);
      assertSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void takeWhileIntegratorIsNotGreedy() {
      var gatherer = GathererDemo.takeWhile(x -> true);
      assertFalse(gatherer.integrator() instanceof Gatherer.Integrator.Greedy);
    }

    @Test
    public void takeWhileSignature() {
      var list = List.of("a", "b", "c", "dd", "e");
      var result = list.stream()
          .gather(GathererDemo.takeWhile((Object o) -> o.toString().length() == 1))
          .toList();
      assertEquals(List.of("a", "b", "c"), result);
    }

    @Test
    public void takeWhilePrecondition() {
      assertThrows(NullPointerException.class, () -> GathererDemo.takeWhile(null));
    }
  }


  @Nested
  public class Q4 {
    @Test
    public void indexed() {
      var list = List.of("foo", "bar");
      var result = list.stream().gather(GathererDemo.indexed()).toList();
      var expected = List.of(new GathererDemo.Indexed<>("foo", 0), new GathererDemo.Indexed<>("bar", 1));
      assertEquals(expected, result);
    }

    @Test
    public void indexedWithSingleIntegerElement() {
      var list = List.of(100);
      var result = list.stream().gather(GathererDemo.indexed()).toList();
      var expected = List.of(new GathererDemo.Indexed<>(100, 0));
      assertEquals(expected, result);
    }

    @Test
    public void indexedAndCollectToSet() {
      var list = List.of(2, 4);
      var result = list.stream()
          .gather(GathererDemo.indexed())
          .collect(Collectors.toSet());
      assertEquals(Set.of(new GathererDemo.Indexed<>(2, 0), new GathererDemo.Indexed<>(4, 1)), result);
    }

    @Test
    public void indexedTenValues() {
      var list = IntStream.range(0, 10).boxed().toList();
      var result = list.stream().gather(GathererDemo.indexed()).toList();
      var expected = IntStream.range(0, 10).mapToObj(i -> new GathererDemo.Indexed<>(i, i)).toList();
      assertEquals(expected, result);
    }

    @Test
    public void indexedWithNullElement() {
      var list = Arrays.asList("foo", null, "bar");
      var result = list.stream().gather(GathererDemo.indexed()).toList();
      var expected = List.of(
          new GathererDemo.Indexed<>("foo", 0),
          new GathererDemo.Indexed<>(null, 1),
          new GathererDemo.Indexed<>("bar", 2)
      );
      assertEquals(expected, result);
    }

    @Test
    public void indexedWithParallelStream() {
      var list = IntStream.range(0, 100).boxed().toList();
      var result = list.parallelStream()
          .gather(GathererDemo.indexed())
          .toList();
      assertEquals(100, result.size());
      var indices = result.stream().map(GathererDemo.Indexed::index).toList();
      assertEquals(list, indices);
    }

    @Test
    public void indexedGathererAreIndependent() {
      var gatherer = GathererDemo.indexed();
      var list = List.of("foo", "bar", "baz");
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void indexedPropagationWorks() {
      var list = IntStream.range(0, 10).boxed().toList();
      var result = list.stream().gather(GathererDemo.indexed()).limit(3).toList();
      var expected = List.of(
          new GathererDemo.Indexed<>(0, 0),
          new GathererDemo.Indexed<>(1, 1),
          new GathererDemo.Indexed<>(2, 2));
      assertEquals(expected, result);
    }

    @Test
    public void indexedIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.indexed();
      assertSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void indexedIntegratorIsGreedy() {
      var gatherer = GathererDemo.indexed();
      assertInstanceOf(Gatherer.Integrator.Greedy.class, gatherer.integrator());
    }

    @Test
    public void indexedConstructorRejectsNegativeIndex() {
      assertThrows(IllegalArgumentException.class,
          () -> new GathererDemo.Indexed<>("hello", -1));
    }

    @Test
    public void indexedConstructorRejectsLargeNegativeIndex() {
      assertThrows(IllegalArgumentException.class,
          () -> new GathererDemo.Indexed<>("hello", Integer.MIN_VALUE));
    }

    @Test
    public void indexedIsARecord() {
      var type = GathererDemo.Indexed.class;
      assertAll(
          () -> assertTrue(type.accessFlags().contains(AccessFlag.PUBLIC)),
          () -> assertTrue(type.accessFlags().contains(AccessFlag.FINAL)),
          () -> assertTrue(type.isRecord())
      );
    }
  }


  @Nested
  public class Q5 {
    @Test
    public void indexedFunctionCreatesIndexedPairs() {
      var list = List.of("foo", "bar");
      var result = list.stream()
          .gather(GathererDemo.indexed(GathererDemo.Indexed::new))
          .toList();
      var expected = List.of(new GathererDemo.Indexed<>("foo", 0), new GathererDemo.Indexed<>("bar", 1));
      assertEquals(expected, result);
    }

    @Test
    public void indexedFunctionWithSingleInteger() {
      var list = List.of(100);
      var result = list.stream()
          .gather(GathererDemo.indexed(GathererDemo.Indexed::new))
          .toList();
      assertEquals(List.of(new GathererDemo.Indexed<>(100, 0)), result);
    }

    @Test
    public void indexedFunctionCollectsToSet() {
      var list = List.of(2, 4);
      var result = list.stream()
          .gather(GathererDemo.indexed(GathererDemo.Indexed::new))
          .collect(Collectors.toSet());
      assertEquals(Set.of(new GathererDemo.Indexed<>(2, 0), new GathererDemo.Indexed<>(4, 1)), result);
    }

    @Test
    public void indexedFunctionTenValues() {
      var list = IntStream.range(0, 10).boxed().toList();
      var result = list.stream()
          .gather(GathererDemo.indexed(GathererDemo.Indexed::new))
          .toList();
      var expected = IntStream.range(0, 10).mapToObj(i -> new GathererDemo.Indexed<>(i, i)).toList();
      assertEquals(expected, result);
    }

    @Test
    public void indexedFunctionOnlyUsesIndex() {
      var list = List.of("ignore", "these", "values");
      var result = list.stream()
          .gather(GathererDemo.indexed((_, index) -> index * 10))
          .toList();
      assertEquals(List.of(0, 10, 20), result);
    }

    @Test
    public void indexedFunctionIdentity() {
      var list = IntStream.range(0, 10).boxed().toList();
      var result = list.stream()
          .gather(GathererDemo.indexed((e, _) -> e))
          .toList();
      assertEquals(list, result);
    }

    @Test
    public void indexedFunctionEmptyList() {
      var result = Stream.<String>of()
          .gather(GathererDemo.indexed(GathererDemo.Indexed::new))
          .toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void indexedFunctionWithNullElement() {
      var list = Arrays.asList("foo", null, "bar");
      var result = list.stream()
          .gather(GathererDemo.indexed(GathererDemo.Indexed::new))
          .toList();
      assertEquals(List.of(
          new GathererDemo.Indexed<>("foo", 0),
          new GathererDemo.Indexed<>(null, 1) ,
          new GathererDemo.Indexed<>("bar", 2)),
          result);
    }

    @Test
    public void indexedFunctionWithParallelStream() {
      var list = IntStream.range(0, 100).boxed().toList();
      var result = list.parallelStream()
          .gather(GathererDemo.indexed((e, _) -> e))
          .toList();
      assertEquals(list, result);
    }

    @Test
    public void indexedFunctionGathererAreIndependent() {
      var gatherer = GathererDemo.indexed((e, _) -> e);
      var list = List.of("foo", "bar", "baz");
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void indexedFunctionIsCorrectlyTyped() {
      List<String> list = List.of("a", "b");
      List<Integer> result = list.stream()
          .gather(GathererDemo.indexed((String s, int i) -> s.length() + i))
          .toList();
      assertEquals(List.of(1, 2), result);
    }

    @Test
    public void indexedFunctionSignature() {
      var list = List.of("a", "b", "c", "dd", "e");
      var result = list.stream()
          .gather(GathererDemo.indexed((Object _, int index) -> index))
          .toList();
      assertEquals(List.of(0, 1, 2, 3, 4), result);
    }

    @Test
    public void indexedFunctionIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.indexed((e, _) -> e);
      assertSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void indexedFunctionIntegratorIsGreedy() {
      var gatherer = GathererDemo.indexed((e, _) -> e);
      assertInstanceOf(Gatherer.Integrator.Greedy.class, gatherer.integrator());
    }

    @Test
    public void indexedFunctionPropagationWorks() {
      var list = IntStream.range(0, 10).boxed().toList();
      var result = list.stream()
          .gather(GathererDemo.indexed(GathererDemo.Indexed::new))
          .limit(3)
          .toList();
      var expected = List.of(
          new GathererDemo.Indexed<>(0, 0),
          new GathererDemo.Indexed<>(1, 1),
          new GathererDemo.Indexed<>(2, 2));
      assertEquals(expected, result);
    }

    @Test
    public void indexedFunctionPrecondition() {
      assertThrows(NullPointerException.class, () -> GathererDemo.indexed(null));
    }

    @Test
    public void indexedFunctionParameterIsAFunctionalInterface() {
      var method = Arrays.stream(GathererDemo.class.getMethods())
          .filter(m -> m.getName().equals("indexed") && m.getParameterCount() == 1)
          .findFirst().orElseThrow();
      var parameterType = method.getParameterTypes()[0];
      assertAll(
          () -> assertTrue(parameterType.accessFlags().contains(AccessFlag.PUBLIC)),
          () -> assertTrue(parameterType.isAnnotationPresent(FunctionalInterface.class))
      );
    }

    @Test
    public void indexedFunctionNoBoxing() {
      var method = Arrays.stream(GathererDemo.class.getMethods())
          .filter(m -> m.getName().equals("indexed") && m.getParameterCount() == 1)
          .findFirst().orElseThrow();
      var parameterType = method.getParameterTypes()[0];
      assertNotEquals(BiFunction.class, parameterType,
          "Should not use a BiFunction to avoid boxing");
    }
  }


  @Nested
  public class Q6 {
    @Test
    public void squashTwoIntegersFourIntegers() {
      var list = List.of(1, 2, 3, 4);
      var result = list.stream()
          .gather(GathererDemo.squashTwoIntegers(Integer::sum))
          .toList();
      assertEquals(List.of(3, 7), result);
    }

    @Test
    public void squashTwoIntegersTwoIntegers() {
      var list = List.of(5, 5);
      var result = list.stream()
          .gather(GathererDemo.squashTwoIntegers(Math::addExact))
          .toList();
      assertEquals(List.of(10), result);
    }

    @Test
    public void squashTwoIntegersAndCollectToSet() {
      var list = List.of(2, 4, 6, 3);
      var result = list.stream()
          .gather(GathererDemo.squashTwoIntegers(Integer::sum))
          .collect(Collectors.toSet());
      assertEquals(Set.of(6, 9), result);
    }

    @Test
    public void squashTwoIntegersEmpty() {
      var result = Stream.<Integer>of()
          .gather(GathererDemo.squashTwoIntegers(Math::addExact))
          .toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void squashTwoIntegersGathererAreIndependent() {
      var gatherer = GathererDemo.squashTwoIntegers(Integer::sum);
      var list = List.of(1, 2, 3, 4);
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void squashTwoIntegersIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.squashTwoIntegers(Integer::sum);
      assertSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void squashTwoIntegersIntegratorIsGreedy() {
      var gatherer = GathererDemo.squashTwoIntegers(Integer::sum);
      assertInstanceOf(Gatherer.Integrator.Greedy.class, gatherer.integrator());
    }

    @Test
    public void squashTwoIntegersPropagationWorks() {
      var list = List.of(1, 2, 3, 4, 6, 1, 2, 3);
      var result = list.stream()
          .gather(GathererDemo.squashTwoIntegers(Integer::sum))
          .limit(3)
          .toList();
      assertEquals(List.of(3, 7, 7), result);
    }

    @Test
    public void squashTwoIntegersPropagationWorks2() {
      var list = List.of(1, 2, 3);
      var result = list.stream()
          .peek(value -> {
            if (value == 3) {
              Assertions.fail("read 3 but this is not necessary");
            }
          })
          .gather(GathererDemo.squashTwoIntegers(Integer::sum))
          .limit(1)
          .toList();
      assertEquals(List.of(3), result);
    }

    @Test
    public void squashTwoIntegersPrecondition() {
      assertThrows(NullPointerException.class, () -> GathererDemo.squashTwoIntegers(null));
    }

    static void fail(Object unused) {
      Assertions.fail("should not be called");
    }

    @Test
    public void squashTwoIntegersPrecondition2() {
      var list = List.of(1, 2, 3);
      assertThrows(IllegalStateException.class, () ->
        fail(list.stream().gather(GathererDemo.squashTwoIntegers(Math::addExact)).toList())
      );
    }

    @Test
    public void qualityOfImplementation() {
      var methods = Arrays.stream(GathererDemo.class.getMethods())
          .filter(m -> m.getName().equals("squashTwoIntegers") && m.getParameterCount() == 1)
          .toList();
      assertFalse(methods.isEmpty());

      for (var method : methods) {
        assertNotEquals(Function.class, method.getParameters()[0].getType());
      }
    }
  }


  @Nested
  public class Q7 {
    @Test
    public void squashTwoIntegersCombinesFourElementsIntoPairs() {
      var list = List.of(1, 2, 3, 4);
      var result = list.stream()
          .gather(GathererDemo.squashTwo(Integer::sum))
          .toList();
      assertEquals(List.of(3, 7), result);
    }

    @Test
    public void squashTwoTwoIntegers() {
      var list = List.of(5, 5);
      var result = list.stream()
          .gather(GathererDemo.squashTwo(Math::addExact))
          .toList();
      assertEquals(List.of(10), result);
    }

    @Test
    public void squashTwoTwoStrings() {
      var list = List.of("foo", "bar");
      var result = list.stream()
          .gather(GathererDemo.squashTwo(String::concat))
          .toList();
      assertEquals(List.of("foobar"), result);
    }

    @Test
    public void squashTwoAndCollectToSet() {
      var list = List.of(2, 4, 6, 3);
      var result = list.stream()
          .gather(GathererDemo.squashTwo(Integer::sum))
          .collect(Collectors.toSet());
      assertEquals(Set.of(6, 9), result);
    }

    @Test
    public void squashTwoIntegersWithMax() {
      var list = List.of(5, 10, 3, 8);
      var result = list.stream()
          .gather(GathererDemo.squashTwoIntegers(Math::max))
          .toList();
      assertEquals(List.of(10, 8), result);
    }

    @Test
    public void squashTwoIntegersWithMin() {
      var list = List.of(5, 10, 3, 8);
      var result = list.stream()
          .gather(GathererDemo.squashTwoIntegers(Math::min))
          .toList();
      assertEquals(List.of(5, 3), result);
    }

    @Test
    public void squashTwoIntegersHandlesEmptyStream() {
      var result = Stream.<String>of()
          .gather(GathererDemo.squashTwo(String::concat))
          .toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void squashTwoGathererAreIndependent() {
      var gatherer = GathererDemo.squashTwo(Integer::sum);
      var list = List.of(1, 2, 3, 4);
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void squashTwoIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.squashTwo(Integer::sum);
      assertSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void squashTwoIntegratorIsGreedy() {
      var gatherer = GathererDemo.squashTwo(Integer::sum);
      assertInstanceOf(Gatherer.Integrator.Greedy.class, gatherer.integrator());
    }

    @Test
    public void squashTwoPropagationWorks() {
      var list = List.of(1, 2, 3, 4, 6, 1, 2, 3);
      var result = list.stream()
          .gather(GathererDemo.squashTwo(Integer::sum))
          .limit(3)
          .toList();
      assertEquals(List.of(3, 7, 7), result);
    }

    @Test
    public void squashTwoSignature() {
      var list = List.of("foo", "bar");
      var result = list.stream()
          .gather(GathererDemo.squashTwo((Object o1, Object o2) -> o1 + "" + o2))
          .toList();
      assertEquals(List.of("foobar"), result);
    }

    @Test
    public void squashTwoWorksWithNull() {
      var list = Arrays.asList(null, null, 1, null);
      var result = list.stream()
          .gather(GathererDemo.squashTwo(Objects::equals))
          .toList();
      assertEquals(List.of(true, false), result);
    }

    @Test
    public void squashTwoDoNotAcceptNull() {
      assertThrows(NullPointerException.class, () -> GathererDemo.squashTwo(null));
    }

    static void fail(Object unused) {
      Assertions.fail("should not be called");
    }

    @Test
    public void squashTwoIntegersRejectsOddElementCount() {
      var list = List.of("foo", "bar", "baz");
      assertThrows(IllegalStateException.class, () ->
        fail(list.stream().gather(GathererDemo.squashTwo(String::concat)).toList())
      );
    }

    @Test
    public void squashTwoIntegersSingleElement() {
      var list = List.of(42);
      assertThrows(IllegalStateException.class, () ->
          list.stream().gather(GathererDemo.squashTwoIntegers(Integer::sum)).toList()
      );
    }
  }


  @Nested
  public class Q8 {
    @Test
    public void windowSquashFourIntegers() {
      var list = List.of(1, 2, 3, 4);
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, identity()))
          .toList();
      assertEquals(List.of(List.of(1, 2), List.of(3, 4)), result);
    }

    @Test
    public void windowSquashThreeIntegers() {
      var list = List.of(1, 2, 3);
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, identity()))
          .toList();
      assertEquals(List.of(List.of(1, 2), List.of(3)), result);
    }

    @Test
    public void windowSquashTwoStrings() {
      var list = List.of("foo", "bar");
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, identity()))
          .toList();
      assertEquals(List.of(List.of("foo", "bar")), result);
    }

    @Test
    public void windowSquashOneIntegers() {
      var list = List.of(5);
      var result = list.stream()
          .gather(GathererDemo.windowSquash(3, identity()))
          .toList();
      assertEquals(List.of(List.of(5)), result);
    }

    @Test
    public void windowSquashAndCollectToSet() {
      var list = List.of(2, 4, 6);
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, identity()))
          .collect(Collectors.toSet());
      assertEquals(Set.of(List.of(2, 4), List.of(6)), result);
    }

    @Test
    public void windowSquashStringEmpty() {
      var result = Stream.<String>of()
          .gather(GathererDemo.windowSquash(2, identity()))
          .toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void windowSquashGathererAreIndependent() {
      var gatherer = GathererDemo.windowSquash(2, List::of);
      var list = List.of(1, 2, 3, 4, 5);
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void windowSquashPropagationWorks() {
      var list = List.of(1, 2, 3, 4, 6, 1, 2, 3);
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, identity()))
          .limit(3)
          .toList();
      assertEquals(List.of(List.of(1, 2), List.of(3, 4), List.of(6, 1)), result);
    }

    @Test
    public void windowSquashIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.windowSquash(2, identity());
      assertSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void windowSquashIntegratorIsGreedy() {
      var gatherer = GathererDemo.windowSquash(2, identity());
      assertInstanceOf(Gatherer.Integrator.Greedy.class, gatherer.integrator());
    }

    @Test
    public void windowSquash() {
      var list = List.of("a", "b", "c", "d");
      var result = list.stream()
          .gather(GathererDemo.windowSquash(3, Collection::size))
          .toList();
      assertEquals(List.of(3, 1), result);
    }

    @Test
    public void windowSquashOfStrings() {
      var list = List.of("foo", "bar", "baz");
      var result = list.stream()
          .gather(GathererDemo.windowSquash(1, List::getFirst))
          .toList();
      assertEquals(list, result);
    }

    @Test
    public void windowSquashCountsElementsInWindow() {
      var list = List.of("a", "bb", "ccc", "dddd", "eeeee");
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, Collection::size))
          .toList();
      assertEquals(List.of(2, 2, 1), result);
    }

    @Test
    public void windowSquashConcatenatesStrings() {
      var list = List.of("a", "b", "c", "d", "e");
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, l -> String.join("-", l)))
          .toList();
      assertEquals(List.of("a-b", "c-d", "e"), result);
    }

    @Test
    public void windowSquashWithWindowSizeLargerThanStream() {
      var list = List.of(1, 2, 3);
      var result = list.stream()
          .gather(GathererDemo.windowSquash(10, identity()))
          .toList();
      assertEquals(List.of(List.of(1, 2, 3)), result);
    }

    @Test
    public void windowSquashWorksWithNull() {
      var list = Arrays.asList(null, null, 1, null);
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, identity()))
          .toList();
      assertEquals(List.of(Arrays.asList(null, null), Arrays.asList(1, null)), result);
    }

    @Test
    public void windowSquashSignature() {
      var list = List.of("foo", "bar", "baz");
      var result = list.stream()
          .gather(GathererDemo.windowSquash(2, (List<?> l) -> l.size()))
          .toList();
      assertEquals(List.of(2, 1), result);
    }

    @Test
    public void windowSquashPreconditions() {
      assertAll(
          () -> assertThrows(IllegalArgumentException.class, () -> GathererDemo.windowSquash(-1, identity())),
          () -> assertThrows(NullPointerException.class, () -> GathererDemo.windowSquash(2, null))
      );
    }
  }


  @Nested
  public class Q9 {
    @Test
    public void windowFixedToList() {
      var list = List.of(1, 2, 3, 4);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.toList()))
          .toList();
      assertEquals(List.of(List.of(1, 2), List.of(3, 4)), result);
    }

    @Test
    public void windowFixedToSet() {
      var list = List.of(1, 2, 3, 4);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.toSet()))
          .toList();
      assertEquals(List.of(Set.of(1, 2), Set.of(3, 4)), result);
    }

    @Test
    public void windowFixedToSetToSet() {
      var list = List.of(1, 2, 3, 4);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.toSet()))
          .collect(Collectors.toSet());
      assertEquals(Set.of(Set.of(1, 2), Set.of(3, 4)), result);
    }

    @Test
    public void windowFixedThreeIntegersToList() {
      var list = List.of(1, 2, 3);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.toList()))
          .toList();
      assertEquals(List.of(List.of(1, 2), List.of(3)), result);
    }

    @Test
    public void windowFixedThreeIntegersToSet() {
      var list = List.of(1, 2, 3);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.toUnmodifiableSet()))
          .toList();
      assertEquals(List.of(Set.of(1, 2), Set.of(3)), result);
    }

    @Test
    public void windowFixedJoining() {
      var list = List.of("a", "b", "c");
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.joining()))
          .toList();
      assertEquals(List.of("ab", "c"), result);
    }

    @Test
    public void windowFixedTwoStrings() {
      var list = List.of("bar", "baz", "foo");
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.groupingBy(s -> s.charAt(0))))
          .toList();
      assertEquals(List.of(Map.of('b', List.of("bar", "baz")), Map.of('f', List.of("foo"))), result);
    }

    @Test
    public void windowFixedOneIntegers() {
      var list = List.of(5);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(3, Collectors.toList()))
          .toList();
      assertEquals(List.of(List.of(5)), result);
    }

    @Test
    public void windowFixedUsingCounting() {
      var list = List.of("a", "b", "c", "d");
      var result = list.stream()
          .gather(GathererDemo.windowFixed(3, Collectors.counting()))
          .toList();
      assertEquals(List.of(3L, 1L), result);
    }

    @Test
    public void windowFixedWithAveraging() {
      var list = List.of(2, 4, 6, 8);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2,
              Collectors.averagingDouble(Integer::doubleValue)))
          .toList();
      assertEquals(List.of(3.0, 7.0), result);
    }

    @Test
    public void windowFixedOfStrings() {
      var list = List.of("foo", "bar", "baz");
      var result = list.stream()
          .gather(GathererDemo.windowFixed(1, Collectors.toList()))
          .toList();
      assertEquals(List.of(List.of("foo"), List.of("bar"), List.of("baz")), result);
    }

    @Test
    public void windowFixedWithWindowSizeLargerThanStream() {
      var list = List.of(1, 2, 3);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(10, Collectors.toList()))
          .toList();
      assertEquals(List.of(List.of(1, 2, 3)), result);
    }

    @Test
    public void windowFixedStringEmpty() {
      var result = Stream.of()
          .gather(GathererDemo.windowFixed(2, Collectors.toList()))
          .toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void windowFixedPreservesGenericTypes() {
      var list = List.of("a", "b", "c", "d");
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.summingInt(String::length)))
          .toList();
      assertEquals(List.of(2, 2), result);
    }

    @Test
    public void windowFixedWithDifferentInputOutputTypes() {
      var list = List.of(1, 2, 3, 4, 5);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2,
              Collectors.mapping(
                  n -> "num:" + n,
                  Collectors.joining(","))))
          .toList();
      assertEquals(List.of("num:1,num:2", "num:3,num:4", "num:5"), result);
    }

    @Test
    public void windowFixedCallsCollectorFinisher() {
      var list = List.of(1, 2, 3, 4);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2,
              Collectors.collectingAndThen(
                  Collectors.toList(),
                  l -> l.stream().mapToInt(Integer::intValue).sum())))
          .toList();

      assertEquals(List.of(3, 7), result);
    }

    @Test
    public void windowFixedWithNullElements() {
      var list = Arrays.asList(1, null, 3, null, 5, 6);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.toList()))
          .toList();
      assertEquals(3, result.size());
      assertAll(
          () -> assertEquals(Arrays.asList(1, null), result.get(0)),
          () -> assertEquals(Arrays.asList(3, null), result.get(1)),
          () -> assertEquals(List.of(5, 6), result.get(2))
      );
    }

    @Test
    public void windowFixedGathererAreIndependent() {
      var gatherer = GathererDemo.windowFixed(1, Collectors.toList());
      var list = List.of(1, 2, 3, 4, 5);
      var result1 = list.stream().gather(gatherer).toList();
      var result2 = list.stream().gather(gatherer).toList();
      assertEquals(result1, result2);
    }

    @Test
    public void windowFixedToSetPropagationWorks() {
      var list = List.of(1, 2, 3, 4, 5, 6);
      var result = list.stream()
          .gather(GathererDemo.windowFixed(2, Collectors.toSet()))
          .limit(2)
          .toList();
      assertEquals(List.of(Set.of(1, 2), Set.of(3, 4)), result);
    }

    @Test
    public void windowFixedSupportsShortCircuit() {
      var list = IntStream.range(0, 100).boxed().toList();
      var result = list.stream()
          .gather(GathererDemo.windowFixed(5, Collectors.toList()))
          .limit(2)
          .toList();
      assertAll(
          () -> assertEquals(2, result.size()),
          () -> assertEquals(List.of(0, 1, 2, 3, 4), result.get(0)),
          () -> assertEquals(List.of(5, 6, 7, 8, 9), result.get(1))
      );
    }

    @Test
    public void windowFixedIsTheRightKindOfGatherer() {
      var gatherer = GathererDemo.windowFixed(2, Collectors.toList());
      assertSame(Gatherer.defaultCombiner(), gatherer.combiner());
    }

    @Test
    public void windowFixedIntegratorIsGreedy() {
      var gatherer = GathererDemo.windowFixed(2, Collectors.toList());
      assertInstanceOf(Gatherer.Integrator.Greedy.class, gatherer.integrator());
    }

    @Test
    public void windowFixedPreconditions() {
      assertAll(
          () -> assertThrows(IllegalArgumentException.class,
              () -> GathererDemo.windowFixed(-1, Collectors.toList())),
          () -> assertThrows(IllegalArgumentException.class,
              () -> GathererDemo.windowFixed(0, Collectors.toSet())),
          () -> assertThrows(NullPointerException.class,
              () -> GathererDemo.windowFixed(2, null))
      );
    }
  }

  @Nested
  public class Q10 {
    @Test
    public void windowFixedToList() {
      var list = List.of(1, 2, 3, 4);
      var result = list.stream()
          .gather(GathererDemo.<Integer, List<Integer>>windowFixed(2, Collectors.toList()))
          .toList();
      assertEquals(List.of(List.of(1, 2), List.of(3, 4)), result);
    }
  }
}