package fr.uge.graph;

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

import java.io.IOException;
import java.lang.classfile.ClassFile;
import java.lang.classfile.Instruction;
import java.lang.classfile.Opcode;
import java.lang.classfile.instruction.InvokeInstruction;
import java.lang.reflect.AccessFlag;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.Spliterator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;

public final class GraphTest {

  @Nested
  public class Q1 {

    @Test
    public void testOfMethodWithZeroNodes() {
      var graph = Graph.of(0);
      assertNotNull(graph);
      assertEquals(0, graph.nodeCount());
    }

    @Test
    public void testOfMethodWithPositiveNodes() {
      var graph = Graph.of(5);
      assertNotNull(graph);
      assertEquals(5, graph.nodeCount());
    }

    @Test
    public void testOfMethodWithLargeNodeCount() {
      var graph = Graph.of(10_000);
      assertNotNull(graph);
      assertEquals(10_000, graph.nodeCount());
    }

    @Test
    public void testMatrixGraphNodeCount() {
      var graph = Graph.of(10);
      assertEquals(10, graph.nodeCount());
    }

    @Test
    public void testMatrixGraphWithDifferentGenericTypes() {
      var stringGraph = Graph.<String>of(2);
      var integerGraph = Graph.<Integer>of(2);
      var doubleGraph = Graph.<Double>of(2);

      assertEquals(2, stringGraph.nodeCount());
      assertEquals(2, integerGraph.nodeCount());
      assertEquals(2, doubleGraph.nodeCount());
    }

    @Test
    public void testGraphInstanceType() {
      var graph = Graph.of(3);
      assertInstanceOf(MatrixGraph.class, graph);
    }

    @Test
    public void testGraphIsAnInterface() {
      assertTrue(Graph.class.isInterface());
    }

    @Test
    public void testGraphHasNoStaticFields() {
      assertEquals(0, Graph.class.getDeclaredFields().length);
    }

    @Test
    public void testGraphHasNoSupplementaryPublicMethods() {
      var authorizedNames = Set.of("of", "nodeCount", "edges", "edgeStream", "getWeight", "addEdge");
      var methods = Arrays.stream(Graph.class.getDeclaredMethods())
          .filter(method -> method.accessFlags().contains(AccessFlag.PUBLIC))
          .map(Method::getName)
          .toList();
      for(var method : methods) {
        assertTrue(authorizedNames.contains(method),
            "Method " + method + " is not authorized in Graph");
      }
    }

    @Test
    public void testGraphMethodsOfDoNotReturnANonVisibleType() {
      assertTrue(Arrays.stream(Graph.class.getDeclaredMethods())
          .filter(method -> method.getName().equals("of"))
          .map(Method::getReturnType)
          .allMatch(returnType -> returnType.accessFlags().contains(AccessFlag.PUBLIC)));
    }

    @Test
    public void testMatrixGraphClass() {
      assertFalse(MatrixGraph.class.accessFlags().contains(AccessFlag.PUBLIC));
      assertTrue(MatrixGraph.class.accessFlags().contains(AccessFlag.FINAL));
    }

    @Test
    public void testMatrixGraphConstructorsCallSuperAsLastInstruction() throws IOException {
      var className = "/" + MatrixGraph.class.getName().replace('.', '/') + ".class";
      byte[] data;
      try (var input = MatrixGraph.class.getResourceAsStream(className)) {
        data = input.readAllBytes();
      }
      var classModel = ClassFile.of().parse(data);
      var constructors =
          classModel.methods().stream().filter(m -> m.methodName().equalsString("<init>")).toList();
      for (var constructor : constructors) {
        var code =
            constructor.code().orElseThrow(() -> new AssertionError("Constructor has no code"));
        var instructions =
            code.elementStream()
                .flatMap(e -> e instanceof Instruction instruction ? Stream.of(instruction) : null)
                .toList();
        var lastInstruction =
            instructions.get(instructions.size() - 2); // -2 because last is RETURN
        if (!(lastInstruction instanceof InvokeInstruction invokeInstruction)) {
          throw new AssertionError(
              "lastInstruction is neither super() nor this() " + lastInstruction);
        }
        assertAll(
            () -> assertEquals(Opcode.INVOKESPECIAL, invokeInstruction.opcode()),
            () -> assertEquals("<init>", invokeInstruction.name().stringValue())
        );
      }
    }

    @Test
    public void testMatrixGraphHasNoPublicConstructor() {
      assertEquals(0, MatrixGraph.class.getConstructors().length);
    }

    @Test
    public void testMatrixGraphHasNoSupplementaryPublicMethods() {
      var authorizedNames = Set.of("of", "nodeCount", "edges", "edgeStream", "getWeight", "addEdge");
      var methods = Arrays.stream(MatrixGraph.class.getDeclaredMethods())
          .filter(method -> method.accessFlags().contains(AccessFlag.PUBLIC))
          .map(Method::getName)
          .toList();
      for(var method : methods) {
        assertTrue(authorizedNames.contains(method),
            "Method " + method + " is not authorized in Graph");
      }
    }

    @Test
    public void testMatrixGraphIsTheOnlyImplementation() {
      var permittedSubClasses = Graph.class.getPermittedSubclasses();
      assertNotNull(permittedSubClasses);
      assertEquals(1, permittedSubClasses.length);
      assertEquals(MatrixGraph.class, permittedSubClasses[0]);
    }
  }


  @Nested
  public class Q2 {

    @Test
    public void testGetWeightValidNodes() {
      var graph = Graph.of(3);
      assertNull(graph.getWeight(0, 0));
      assertNull(graph.getWeight(0, 2));
      assertNull(graph.getWeight(2, 1));
    }

    @Test
    public void testGetWeightSingleNodeGraph() {
      var graph = Graph.of(1);
      assertEquals(1, graph.nodeCount());
      assertNull(graph.getWeight(0, 0));
    }

    @Test
    public void testGetWeightBoundaryValues() {
      var graph = Graph.of(5);
      assertNull(graph.getWeight(0, 0));
      assertNull(graph.getWeight(0, 4));
      assertNull(graph.getWeight(4, 0));
      assertNull(graph.getWeight(4, 4));
    }

    @Test
    public void testGetWeightWithDifferentGenericTypes() {
      var stringGraph = Graph.<String>of(2);
      var integerGraph = Graph.<Integer>of(2);
      var doubleGraph = Graph.<Double>of(2);

      String stringValue = stringGraph.getWeight(0, 1);
      Integer integerValue = integerGraph.getWeight(1, 0);
      Double doubleValue = doubleGraph.getWeight(0, 0);

      assertNull(stringValue);
      assertNull(integerValue);
      assertNull(doubleValue);
    }

    @Test
    public void testOfWithDefaultValueValidNodeCount() {
      var graph = Graph.of(3, "default");
      assertNotNull(graph);
      assertEquals(3, graph.nodeCount());
    }

    @Test
    public void testOfWithDefaultValueZeroNodes() {
      var graph = Graph.of(0, 42);
      assertNotNull(graph);
      assertEquals(0, graph.nodeCount());
    }

    @Test
    public void testDifferentGenericTypesWithDefaultValues() {
      Graph<String> stringGraph = Graph.of(2, "default");
      Graph<Integer> integerGraph = Graph.of(2, 100);
      Graph<Boolean> booleanGraph = Graph.of(2, true);

      String stringGraphWeight = stringGraph.getWeight(0, 1);
      int integerGraphWeight = integerGraph.getWeight(1, 0);
      boolean booleanGraphWeight = booleanGraph.getWeight(1, 1);

      assertEquals("default", stringGraphWeight);
      assertEquals(100, integerGraphWeight);
      assertTrue(booleanGraphWeight);
    }

    @Test
    public void testDifferentGenericTypesWithGetWeight() {
      var stringGraph = Graph.of(2, "");

      assertTrue(stringGraph.getWeight(0, 1).isEmpty());
    }

    @Test
    public void testOfWithDefaultValueThrowsOnNegativeNodeCount() {
      var exception = assertThrows(IllegalArgumentException.class,
          () -> Graph.of(-1, "default"));
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testOfWithDefaultValueThrowsOnLargeNegativeNodeCount() {
      var exception = assertThrows(IllegalArgumentException.class,
          () -> Graph.of(-100, "default"));
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testGetWeightReturnsDefaultValue() {
      var graph = Graph.of(3, "default");
      assertEquals("default", graph.getWeight(0, 0));
      assertEquals("default", graph.getWeight(0, 2));
      assertEquals("default", graph.getWeight(2, 1));
    }

    @Test
    public void testGetWeightReturnsNullDefaultValue() {
      var graph = Graph.of(3, null);
      assertNull(graph.getWeight(0, 0));
      assertNull(graph.getWeight(1, 2));
      assertNull(graph.getWeight(2, 0));
    }

    @Test
    public void testGetWeightWithIntegerDefaultValue() {
      var graph = Graph.of(2, 42);
      assertEquals(42, graph.getWeight(0, 0));
      assertEquals(42, graph.getWeight(0, 1));
      assertEquals(42, graph.getWeight(1, 0));
      assertEquals(42, graph.getWeight(1, 1));
    }

    @Test
    public void testGetWeightWithDoubleDefaultValue() {
      var graph = Graph.of(2, 3.14);
      assertEquals(3.14, graph.getWeight(0, 0));
      assertEquals(3.14, graph.getWeight(1, 0));
    }

    @Test
    public void testGetWeightThrowsOnInvalidSrc() {
      var graph = Graph.of(3, "default");
      var exception1 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.getWeight(-1, 0));
      assertNotNull(exception1.getMessage());

      var exception2 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.getWeight(3, 0));
      assertNotNull(exception2.getMessage());
    }

    @Test
    public void testGetWeightThrowsOnInvalidDst() {
      var graph = Graph.of(3, "default");
      var exception1 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.getWeight(0, -1));
      assertNotNull(exception1.getMessage());

      var exception2 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.getWeight(0, 3));
      assertNotNull(exception2.getMessage());
    }

    @Test
    public void testGraphInstanceTypeWithDefaultValue() {
      var graph = Graph.of(3, "test");
      assertInstanceOf(MatrixGraph.class, graph);
    }

    @Test
    public void testSingleNodeGraphWithDefaultValue() {
      var graph = Graph.of(1, 99);
      assertEquals(1, graph.nodeCount());
      assertEquals(99, graph.getWeight(0, 0));
    }

    @Test
    public void testMediumSizeGraphWithDefaultValue() {
      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        var graph = Graph.of(1_000, "large");
        assertEquals(1_000, graph.nodeCount());
        assertEquals("large", graph.getWeight(0, 0));
        assertEquals("large", graph.getWeight(500, 750));
        assertEquals("large", graph.getWeight(999, 99));
      });
    }

    @Test
    public void testLargeGraphWithNull() {
      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        var graph = Graph.of(10_000, null);
        assertEquals(10_000, graph.nodeCount());
        assertNull(graph.getWeight(0, 0));
        assertNull(graph.getWeight(5_000, 7_500));
        assertNull(graph.getWeight(9_999, 999));
      });
    }
  }


  @Nested
  public class Q3 {

    @Test
    public void testAddEdgeValidParameters() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "weight1");
      assertEquals("weight1", graph.getWeight(0, 1));
    }

    @Test
    public void testAddEdgeOverwritesPreviousWeight() {
      var graph = Graph.of(3, "default");
      graph.addEdge(1, 2, "first");
      graph.addEdge(1, 2, "second");
      assertEquals("second", graph.getWeight(1, 2));
    }

    @Test
    public void testAddEdgeDoesNotAffectOtherPositions() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "weight");
      assertEquals("weight", graph.getWeight(0, 1));
      assertEquals("default", graph.getWeight(0, 0));
      assertEquals("default", graph.getWeight(1, 0));
      assertEquals("default", graph.getWeight(2, 2));
    }

    @Test
    public void testAddEdgeSingleNodeGraph() {
      var graph = Graph.of(1, "default");
      graph.addEdge(0, 0, "self");
      assertEquals("self", graph.getWeight(0, 0));
    }

    @Test
    public void testAddEdgeThrowsOnInvalidSrc() {
      var graph = Graph.of(3, "default");
      var exception1 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.addEdge(-1, 0, "weight"));
      assertNotNull(exception1.getMessage());

      var exception2 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.addEdge(3, 0, "weight"));
      assertNotNull(exception2.getMessage());
    }

    @Test
    public void testAddEdgeThrowsOnInvalidDst() {
      var graph = Graph.of(3, "default");
      var exception1 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.addEdge(0, -1, "weight"));
      assertNotNull(exception1.getMessage());

      var exception2 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.addEdge(0, 3, "weight"));
      assertNotNull(exception2.getMessage());
    }

    @Test
    public void testAddEdgeThrowsOnNull() {
      var graph = Graph.of(3, null);
      assertThrows(NullPointerException.class,
          () -> graph.addEdge(0, 1, null));
    }

    @Test
    public void testAddEdgeThrowsOnNullDefaultValue() {
      var graph = Graph.of(3, "default");
      assertThrows(NullPointerException.class,
          () -> graph.addEdge(0, 1, null));
    }

    @Test
    public void testAddEdgeThrowsOnDefaultValue() {
      var graph = Graph.of(3, "default");
      var exception = assertThrows(IllegalArgumentException.class,
          () -> graph.addEdge(0, 1, "default"));
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testAddEdgeThrowsOnDefaultIntegerValue() {
      var graph = Graph.of(3, 1024);
      var exception = assertThrows(IllegalArgumentException.class,
          () -> graph.addEdge(0, 1, 1024));
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testAddEdgeThrowsOnDefaultDoubleValue() {
      var graph = Graph.of(2, 3.14);
      var exception = assertThrows(IllegalArgumentException.class,
          () -> graph.addEdge(1, 0, 3.14));
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testAddEdgeWithIntegerDefault() {
      var graph = Graph.of(3, 42);
      graph.addEdge(0, 1, 100);
      assertEquals(100, graph.getWeight(0, 1));
      assertEquals(42, graph.getWeight(0, 0));
    }

    @Test
    public void testAddEdgeWithDoubleDefault() {
      var graph = Graph.of(2, 3.14);
      graph.addEdge(0, 1, 2.71);
      assertEquals(2.71, graph.getWeight(0, 1));
    }

    @Test
    public void testAddEdgeSelfLoop() {
      var graph = Graph.of(3, "default");
      graph.addEdge(1, 1, "loop");
      assertEquals("loop", graph.getWeight(1, 1));
      assertEquals("default", graph.getWeight(0, 0));
      assertEquals("default", graph.getWeight(2, 2));
    }

    @Test
    public void testAddEdgeAllPositions() {
      var graph = Graph.of(2, 0);
      graph.addEdge(0, 0, 1);
      graph.addEdge(0, 1, 2);
      graph.addEdge(1, 0, 3);
      graph.addEdge(1, 1, 4);

      assertEquals(1, graph.getWeight(0, 0));
      assertEquals(2, graph.getWeight(0, 1));
      assertEquals(3, graph.getWeight(1, 0));
      assertEquals(4, graph.getWeight(1, 1));
    }

    @Test
    public void testAddEdgeBoundaryNodes() {
      var graph = Graph.of(5, "default");
      graph.addEdge(0, 4, "edge1");
      graph.addEdge(4, 0, "edge2");

      assertEquals("edge1", graph.getWeight(0, 4));
      assertEquals("edge2", graph.getWeight(4, 0));
      assertEquals("default", graph.getWeight(2, 2));
    }

    @Test
    public void testAddEdgeWithDifferentTypes() {
      var stringGraph = Graph.of(2, "default");
      var booleanGraph = Graph.of(2, false);

      stringGraph.addEdge(0, 1, "custom");
      booleanGraph.addEdge(0, 1, true);

      assertEquals("custom", stringGraph.getWeight(0, 1));
      assertEquals(true, booleanGraph.getWeight(0, 1));
    }

    @Test
    public void testAddEdgeSequentialOperations() {
      var graph = Graph.of(3, 0);

      // Add multiple edges
      graph.addEdge(0, 1, 10);
      graph.addEdge(1, 2, 20);
      graph.addEdge(2, 0, 30);

      // Verify all edges
      assertEquals(10, graph.getWeight(0, 1));
      assertEquals(20, graph.getWeight(1, 2));
      assertEquals(30, graph.getWeight(2, 0));

      // Verify default values still intact
      assertEquals(0, graph.getWeight(0, 0));
      assertEquals(0, graph.getWeight(1, 1));
      assertEquals(0, graph.getWeight(2, 2));
    }
  }


  @Nested
  public class Q4 {

    @Test
    public void testEdgeConstructor() {
      var edge = new Graph.Edge<>(0, 5, "test");
      assertEquals(0, edge.src());
      assertEquals(5, edge.dst());
      assertEquals("test", edge.weight());
    }

    @Test
    public void testEdgeEquality() {
      var edge1 = new Graph.Edge<>(1, 2, "weight");
      var edge2 = new Graph.Edge<>(1, 2, "weight");
      var edge3 = new Graph.Edge<>(2, 1, "weight");

      assertEquals(edge1, edge2);
      assertNotEquals(edge1, edge3);
      assertEquals(edge1.hashCode(), edge2.hashCode());
    }

    @Test
    public void testEdgeToString() {
      var edge = new Graph.Edge<>(1, 2, "weight");
      var toString = edge.toString();
      assertTrue(toString.contains("1"));
      assertTrue(toString.contains("2"));
      assertTrue(toString.contains("weight"));
    }

    @Test
    public void testEdgeConstructorThrowsOnNegativeSrc() {
      var exception = assertThrows(IllegalArgumentException.class,
          () -> new Graph.Edge<>(-1, 5, "test"));
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testEdgeConstructorThrowsOnNegativeDst() {
      var exception = assertThrows(IllegalArgumentException.class,
          () -> new Graph.Edge<>(0, -1, "test"));
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testEdgeConstructorThrowsOnBothNegative() {
      var exception = assertThrows(IllegalArgumentException.class,
          () -> new Graph.Edge<>(-1, -2, "test"));
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testEdgeConstructorWithZeroNodes() {
      var edge = new Graph.Edge<>(0, 0, 42);
      assertEquals(0, edge.src());
      assertEquals(0, edge.dst());
      assertEquals(42, edge.weight());
    }

    @Test
    public void testEdgeClass() {
      assertTrue(Graph.Edge.class.accessFlags().contains(AccessFlag.PUBLIC));
      assertTrue(Graph.Edge.class.accessFlags().contains(AccessFlag.FINAL));
      assertTrue(Graph.Edge.class.accessFlags().contains(AccessFlag.STATIC));
    }

    @Test
    public void testGraphHasOnlyOneNestedClass() {
      var nestedClasses = Graph.class.getDeclaredClasses();
      assertEquals(1, nestedClasses.length);
      assertEquals(Graph.Edge.class, nestedClasses[0]);
    }


    @Test
    public void testEdgesEmptyForNodeWithNoEdges() {
      var graph = Graph.of(3, "default");
      var edges = graph.edges(0);

      for (var edge : edges) {
        fail("Unexpected edge: " + edge);
      }
    }

    @Test
    public void testEdgesReturnsSingleEdge() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "weight1");

      var edgeList = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(0)) {
        edgeList.add(edge);
      }

      assertEquals(List.of(new Graph.Edge<>(0, 1, "weight1")), edgeList);
    }

    @Test
    public void testEdgesReturnsMultipleEdges() {
      var graph = Graph.of(4, "default");
      graph.addEdge(0, 1, "weight1");
      graph.addEdge(0, 3, "weight3");

      var edgeList = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(0)) {
        edgeList.add(edge);
      }

      assertEquals(2, edgeList.size());
      assertEquals(new Graph.Edge<>(0, 1, "weight1"), edgeList.get(0));
      assertEquals(new Graph.Edge<>(0, 3, "weight3"), edgeList.get(1));
    }

    @Test
    public void testEdgesReturnsEdgesInOrder() {
      var graph = Graph.of(5, 0);
      graph.addEdge(1, 0, 10);
      graph.addEdge(1, 2, 20);
      graph.addEdge(1, 4, 40);

      var edgeList = new ArrayList<Graph.Edge<Integer>>();
      for (var edge : graph.edges(1)) {
        edgeList.add(edge);
      }

      assertEquals(3, edgeList.size());
      assertEquals(new Graph.Edge<>(1, 0, 10), edgeList.get(0));
      assertEquals(new Graph.Edge<>(1, 2, 20), edgeList.get(1));
      assertEquals(new Graph.Edge<>(1, 4, 40), edgeList.get(2));
    }

    @Test
    public void testEdgesSkipsDefaultValues() {
      var graph = Graph.of(4, "default");
      graph.addEdge(0, 1, "weight1");
      // position 0,2 is default
      graph.addEdge(0, 3, "weight3");

      var edgeList = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(0)) {
        edgeList.add(edge);
      }

      assertEquals(2, edgeList.size());
      assertEquals(1, edgeList.get(0).dst());
      assertEquals(3, edgeList.get(1).dst());
    }

    @Test
    public void testEdgesWithSelfLoop() {
      var graph = Graph.of(3, "default");
      graph.addEdge(1, 1, "loop");

      var edgeList = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(1)) {
        edgeList.add(edge);
      }

      assertEquals(List.of(new Graph.Edge<>(1, 1, "loop")), edgeList);
    }

    @Test
    public void testEdgesWithAllPositionsFilled() {
      var graph = Graph.of(3, 0);
      graph.addEdge(0, 0, 1);
      graph.addEdge(0, 1, 2);
      graph.addEdge(0, 2, 3);

      var edgeList = new ArrayList<Graph.Edge<Integer>>();
      for (var edge : graph.edges(0)) {
        edgeList.add(edge);
      }

      assertEquals(3, edgeList.size());
      assertEquals(new Graph.Edge<>(0, 0, 1), edgeList.get(0));
      assertEquals(new Graph.Edge<>(0, 1, 2), edgeList.get(1));
      assertEquals(new Graph.Edge<>(0, 2, 3), edgeList.get(2));
    }

    @Test
    public void testEdgesFromDifferentNodes() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "edge01");
      graph.addEdge(1, 2, "edge12");
      graph.addEdge(2, 0, "edge20");

      var edges0 = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(0)) {
        edges0.add(edge);
      }
      assertEquals(1, edges0.size());
      assertEquals("edge01", edges0.getFirst().weight());

      var edges1 = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(1)) {
        edges1.add(edge);
      }
      assertEquals(1, edges1.size());
      assertEquals("edge12", edges1.getFirst().weight());

      var edges2 = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(2)) {
        edges2.add(edge);
      }
      assertEquals(1, edges2.size());
      assertEquals("edge20", edges2.getFirst().weight());
    }

    @Test
    public void testEdgesThrowsOnInvalidSrc() {
      var graph = Graph.of(3, "default");
      var exception1 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.edges(-1));
      assertNotNull(exception1.getMessage());

      var exception2 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.edges(3));
      assertNotNull(exception2.getMessage());
    }

    @Test
    public void testEdgesSingleNodeGraph() {
      var graph = Graph.of(2, 3.14);
      graph.addEdge(0, 1, 42.0);

      var edgeList = new ArrayList<Graph.Edge<Double>>();
      for (var edge : graph.edges(0)) {
        edgeList.add(edge);
      }

      assertEquals(1, edgeList.size());
      var edge = edgeList.getFirst();
      assertEquals(new Graph.Edge<>(0, 1, 42.0), edge);
    }

    @Test
    public void testEdgesWithNullDefaultValue() {
      var graph = Graph.of(3, (String) null);
      graph.addEdge(0, 1, "weight");

      var edgeList = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(0)) {
        edgeList.add(edge);
      }

      assertEquals(1, edgeList.size());
      assertEquals("weight", edgeList.getFirst().weight());
    }

    @Test
    public void testEdgesWithDifferentDataTypes() {
      var intGraph = Graph.of(3, 0);
      intGraph.addEdge(0, 1, 42);

      var intEdgeList = new ArrayList<Graph.Edge<Integer>>();
      for (var edge : intGraph.edges(0)) {
        intEdgeList.add(edge);
      }
      assertEquals(42, intEdgeList.getFirst().weight());

      var boolGraph = Graph.of(2, false);
      boolGraph.addEdge(0, 1, true);

      var boolEdgeList = new ArrayList<Graph.Edge<Boolean>>();
      for (var edge : boolGraph.edges(0)) {
        boolEdgeList.add(edge);
      }
      assertEquals(true, boolEdgeList.getFirst().weight());
    }

    @Test
    public void testHasNextDoesNotMutateTheIteratorFields() {
      class Dumper {
        static Map<String, Object> getData(Object o) {
          return Arrays.stream(o.getClass().getDeclaredFields())
              .collect(Collectors.toMap(Field::getName, field -> {
                field.setAccessible(true);
                try {
                  return field.get(o);
                } catch (IllegalAccessException e) {
                  throw new AssertionError(e);
                }
              }));
        }
      }

      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "edge1");
      graph.addEdge(0, 2, "edge2");

      var iterator = graph.edges(0).iterator();
      var map1 = Dumper.getData(iterator);

      assertTrue(iterator.hasNext());

      var map2 = Dumper.getData(iterator);

      assertEquals(map1, map2);
    }

    @Test
    public void testEdgesIterableCanBeUsedMultipleTimes() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "weight1");
      graph.addEdge(0, 2, "weight2");

      var edges = graph.edges(0);

      var firstIteration = new ArrayList<Graph.Edge<String>>();
      for (var edge : edges) {
        firstIteration.add(edge);
      }

      var secondIteration = new ArrayList<Graph.Edge<String>>();
      for (var edge : edges) {
        secondIteration.add(edge);
      }

      assertEquals(2, firstIteration.size());
      assertEquals(2, secondIteration.size());
      assertEquals(firstIteration, secondIteration);
    }

    @Test
    public void testEdgesIteratorHasNextIdempotent() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "weight");

      var iterator = graph.edges(0).iterator();
      assertTrue(iterator.hasNext());
      assertTrue(iterator.hasNext());
      assertTrue(iterator.hasNext());

      var edge = iterator.next();
      assertEquals(0, edge.src());
      assertEquals(1, edge.dst());
      assertEquals("weight", edge.weight());

      assertFalse(iterator.hasNext());
      assertFalse(iterator.hasNext());
    }

    @Test
    public void testEdgesIteratorThrowsNoSuchElementException() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "weight");

      var iterator = graph.edges(0).iterator();
      iterator.next();

      var exception = assertThrows(NoSuchElementException.class,
          iterator::next);
      assertNotNull(exception.getMessage());
    }
  }


  @Nested
  public class Q5 {

    @Test
    public void testRemoveAfterNextRevertToDefault() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "edge1");
      graph.addEdge(0, 2, "edge2");

      var iterator = graph.edges(0).iterator();
      iterator.next();
      iterator.remove();

      assertEquals("default", graph.getWeight(0, 1));
    }

    @Test
    public void testRemoveAfterNextDoNotRemoveTheOtherEdges() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "edge1");
      graph.addEdge(0, 2, "edge2");

      var iterator = graph.edges(0).iterator();
      iterator.next();
      iterator.remove();

      var remainingEdges = new ArrayList<Graph.Edge<String>>();
      graph.edges(0).forEach(remainingEdges::add);
      assertEquals(List.of(new Graph.Edge<>(0, 2, "edge2")), remainingEdges);
    }

    @Test
    public void testIteratorStateAfterRemove() {
      var graph = Graph.of(4, 0);
      graph.addEdge(2, 1, 10);
      graph.addEdge(2, 3, 20);

      var iterator = graph.edges(2).iterator();

      assertTrue(iterator.hasNext());
      iterator.next();

      // Should still be able to continue iteration
      assertTrue(iterator.hasNext());
      var secondEdge = iterator.next();
      assertEquals(20, secondEdge.weight());

      // No more edges
      assertFalse(iterator.hasNext());
    }

    @Test
    public void testRemoveMultipleEdges() {
      var graph = Graph.of(4);
      graph.addEdge(1, 0, "A");
      graph.addEdge(1, 2, "B");
      graph.addEdge(1, 3, "C");

      var iterator = graph.edges(1).iterator();

      var firstEdge = iterator.next();
      iterator.remove();
      assertNull(graph.getWeight(1, firstEdge.dst()));

      var secondEdge = iterator.next();
      iterator.remove();
      assertNull(graph.getWeight(1, secondEdge.dst()));

      assertTrue(iterator.hasNext());
      var thirdEdge = iterator.next();
      assertEquals("C", thirdEdge.weight());
    }

    @Test
    public void testRemoveWithSingleEdge() {
      var graph = Graph.of(2, 42);
      graph.addEdge(1, 0, 100);

      var iterator = graph.edges(1).iterator();
      iterator.next();
      iterator.remove();
      assertEquals(42, graph.getWeight(1, 0));

      assertFalse(iterator.hasNext());
    }

    @Test
    public void testRemoveWithSingleEdgeNullDefault() {
      var graph = Graph.<String>of(2);
      graph.addEdge(0, 1, "test");

      var iterator = graph.edges(0).iterator();
      iterator.next();
      iterator.remove();
      assertNull(graph.getWeight(0, 1));

      var newIterator = graph.edges(0).iterator();
      assertFalse(newIterator.hasNext());
    }

    @Test
    public void testRemoveAllWithTwoEdges() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 0, "self");
      graph.addEdge(0, 2, "other");

      var iterator = graph.edges(0).iterator();

      // Remove first edge
      var firstEdge = iterator.next();
      iterator.remove();
      assertEquals("default", graph.getWeight(0, firstEdge.dst()));

      // Move to next edge and remove it
      var secondEdge = iterator.next();
      iterator.remove();
      assertEquals("default", graph.getWeight(0, secondEdge.dst()));

      // No more edges should exist
      assertFalse(iterator.hasNext());
    }

    @Test
    public void testRemoveAllWithALargeNumberOfEdges() {
      var graph = Graph.of(1_000, "default");
      for(int i = 0; i < 1_000; i++) {
        graph.addEdge(0, i, "edge" + i);
      }

      var iterator = graph.edges(0).iterator();

      var count = 0;
      while(iterator.hasNext()) {
        assertEquals("edge" + count, iterator.next().weight());
        iterator.remove();
        count++;
      }

      assertEquals(1_000, count);
    }

    @Test
    public void testRemoveThrowsIllegalStateExceptionWhenCalledBeforeNext() {
      var graph = Graph.of(2, "default");
      graph.addEdge(0, 1, "edge");

      var iterator = graph.edges(0).iterator();

      var exception = assertThrows(IllegalStateException.class, iterator::remove);
      assertNotNull(exception.getMessage());
    }

    @Test
    public void testRemoveThrowsIllegalStateExceptionWhenCalledTwice() {
      var graph = Graph.of(2, "default");
      graph.addEdge(0, 1, "edge");

      var iterator = graph.edges(0).iterator();
      iterator.next();
      iterator.remove();

      var exception = assertThrows(IllegalStateException.class, iterator::remove);
      assertNotNull(exception.getMessage());
    }
  }


  @Nested
  public class Q6 {

    @Test
    public void testEdgeStreamEmptyForNodeWithNoEdges() {
      var graph = Graph.of(3, "default");
      assertEquals(0L, graph.edgeStream(0).count());
    }

    @Test
    public void testEdgeStreamReturnsSingleEdge() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "weight1");

      var edges = graph.edgeStream(0).toList();

      assertEquals(1, edges.size());
      var edge = edges.getFirst();
      assertEquals(0, edge.src());
      assertEquals(1, edge.dst());
      assertEquals("weight1", edge.weight());
    }

    @Test
    public void testEdgeStreamReturnsMultipleEdges() {
      var graph = Graph.of(4, "default");
      graph.addEdge(0, 1, "weight1");
      graph.addEdge(0, 3, "weight3");

      var edges = graph.edgeStream(0).toList();

      assertEquals(2, edges.size());
      assertEquals(new Graph.Edge<>(0, 1, "weight1"), edges.get(0));
      assertEquals(new Graph.Edge<>(0, 3, "weight3"), edges.get(1));
    }

    @Test
    public void testEdgeStreamReturnsEdgesInOrder() {
      var graph = Graph.of(5, 0);
      graph.addEdge(1, 0, 10);
      graph.addEdge(1, 2, 20);
      graph.addEdge(1, 4, 40);

      var edges = graph.edgeStream(1).toList();

      assertEquals(3, edges.size());
      assertEquals(new Graph.Edge<>(1, 0, 10), edges.get(0));
      assertEquals(new Graph.Edge<>(1, 2, 20), edges.get(1));
      assertEquals(new Graph.Edge<>(1, 4, 40), edges.get(2));
    }

    @Test
    public void testEdgeStreamSkipsDefaultValues() {
      var graph = Graph.of(4, "default");
      graph.addEdge(0, 1, "weight1");
      // position 0,2 remains default
      graph.addEdge(0, 3, "weight3");

      var edges = graph.edgeStream(0).toList();

      assertEquals(2, edges.size());
      assertEquals(1, edges.get(0).dst());
      assertEquals(3, edges.get(1).dst());
    }

    @Test
    public void testEdgeStreamWithSelfLoop() {
      var graph = Graph.of(3, "default");
      graph.addEdge(1, 1, "loop");

      var edges = graph.edgeStream(1).toList();

      assertEquals(List.of(new Graph.Edge<>(1, 1, "loop")), edges);
    }

    @Test
    public void testEdgeStreamWithAllPositionsFilled() {
      var graph = Graph.of(3, 0);
      graph.addEdge(0, 0, 1);
      graph.addEdge(0, 1, 2);
      graph.addEdge(0, 2, 3);

      var edges = graph.edgeStream(0).toList();

      assertEquals(3, edges.size());
      assertEquals(new Graph.Edge<>(0, 0, 1), edges.get(0));
      assertEquals(new Graph.Edge<>(0, 1, 2), edges.get(1));
      assertEquals(new Graph.Edge<>(0, 2, 3), edges.get(2));
    }

    @Test
    public void testEdgeStreamThrowsOnInvalidSrc() {
      var graph = Graph.of(3, "default");
      var exception1 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.edgeStream(-1));
      assertNotNull(exception1.getMessage());

      var exception2 = assertThrows(IndexOutOfBoundsException.class,
          () -> graph.edgeStream(3));
      assertNotNull(exception2.getMessage());
    }

    @Test
    public void testEdgeStreamFromDifferentNodes() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 1, "edge01");
      graph.addEdge(1, 2, "edge12");
      graph.addEdge(2, 0, "edge20");

      var edges0 = graph.edgeStream(0).toList();
      assertEquals(1, edges0.size());
      assertEquals("edge01", edges0.getFirst().weight());

      var edges1 = graph.edgeStream(1).toList();
      assertEquals(1, edges1.size());
      assertEquals("edge12", edges1.getFirst().weight());

      var edges2 = graph.edgeStream(2).toList();
      assertEquals(1, edges2.size());
      assertEquals("edge20", edges2.getFirst().weight());
    }

    @Test
    public void testEdgeStreamWithNullDefaultValue() {
      var graph = Graph.of(3, null);
      graph.addEdge(0, 1, "weight");

      var edges = graph.edgeStream(0).toList();

      assertEquals(1, edges.size());
      assertEquals("weight", edges.getFirst().weight());
    }

    @Test
    public void testEdgeStreamSingleNodeGraph() {
      var graph = Graph.of(1, "default");
      graph.addEdge(0, 0, "self");

      var edges = graph.edgeStream(0).toList();

      assertEquals(1, edges.size());
      var edge = edges.getFirst();
      assertEquals(0, edge.src());
      assertEquals(0, edge.dst());
      assertEquals("self", edge.weight());
    }

    @Test
    public void testEdgeStreamWithDifferentDataTypes() {
      var intGraph = Graph.of(3, 0);
      intGraph.addEdge(0, 1, 42);

      var intEdges = intGraph.edgeStream(0).toList();
      assertEquals(42, intEdges.getFirst().weight());

      var boolGraph = Graph.of(2, false);
      boolGraph.addEdge(0, 1, true);

      var boolEdges = boolGraph.edgeStream(0).toList();
      assertEquals(true, boolEdges.getFirst().weight());
    }

    @Test
    public void testEdgeStreamOperations() {
      var graph = Graph.of(5, 0);
      graph.addEdge(0, 1, 10);
      graph.addEdge(0, 2, 20);
      graph.addEdge(0, 3, 30);
      graph.addEdge(0, 4, 40);

      // Test filter operation
      var filteredEdges = graph.edgeStream(0)
          .filter(edge -> edge.weight() > 20)
          .toList();
      assertEquals(2, filteredEdges.size());

      // Test map operation
      var weights = graph.edgeStream(0)
          .map(Graph.Edge::weight)
          .toList();
      assertEquals(List.of(10, 20, 30, 40), weights);

      // Test count operation
      var count = graph.edgeStream(0).count();
      assertEquals(4, count);
    }

    @Test
    public void testEdgeStreamFindFirst() {
      var graph = Graph.of(4, "default");
      graph.addEdge(0, 1, "first");
      graph.addEdge(0, 3, "third");

      var firstEdge = graph.edgeStream(0).findFirst();
      assertTrue(firstEdge.isPresent());
      assertEquals("first", firstEdge.orElseThrow().weight());
      assertEquals(1, firstEdge.orElseThrow().dst());
    }

    @Test
    public void testEdgeStreamFindFirstEmpty() {
      var graph = Graph.of(3, "default");
      var firstEdge = graph.edgeStream(0).findFirst();
      assertFalse(firstEdge.isPresent());
    }

    @Test
    public void testEdgeStreamReduce() {
      var graph = Graph.of(4, 0);
      graph.addEdge(0, 1, 5);
      graph.addEdge(0, 2, 10);
      graph.addEdge(0, 3, 15);

      var sum = graph.edgeStream(0)
          .map(Graph.Edge::weight)
          .reduce(0, Integer::sum);
      assertEquals(30, sum);
    }

    @Test
    public void testEdgeStreamParallel() {
      var graph = Graph.of(4, 0);
      graph.addEdge(0, 1, 1);
      graph.addEdge(0, 2, 2);
      graph.addEdge(0, 3, 3);

      var parallelSum = graph.edgeStream(0)
          .parallel()
          .map(Graph.Edge::weight)
          .reduce(0, Integer::sum);
      assertEquals(6, parallelSum);
    }

    @Test
    public void testEdgeStreamConsistentWithIterable() {
      var graph = Graph.of(4, "default");
      graph.addEdge(0, 1, "weight1");
      graph.addEdge(0, 3, "weight3");

      var streamEdges = graph.edgeStream(0).toList();

      var iterableEdges = new ArrayList<Graph.Edge<String>>();
      for (var edge : graph.edges(0)) {
        iterableEdges.add(edge);
      }

      assertEquals(iterableEdges, streamEdges);
    }

    @Test
    public void testEdgeStreamSpliteratorEstimateSize() {
      var graph = Graph.of(10, null);
      var spliterator = graph.edgeStream(3).spliterator();

      assertTrue(spliterator.estimateSize() >= 0);
      assertTrue(spliterator.estimateSize() <= 10);
    }

    @Test
    public void testEdgeStreamEmptySpliteratorEstimateSize() {
      var graph = Graph.of(10, null);
      var spliterator = graph.edgeStream(3).spliterator();

      spliterator.tryAdvance(_ -> fail());

      assertEquals(0, spliterator.estimateSize());
    }

    @Test
    public void testEdgeStreamSpliteratorCharacteristics() {
      var graph = Graph.of(10, null);
      var spliterator = graph.edgeStream(3).spliterator();

      assertAll(
          () -> assertTrue(spliterator.hasCharacteristics(Spliterator.ORDERED)),
          () -> assertTrue(spliterator.hasCharacteristics(Spliterator.DISTINCT)),
          () -> assertTrue(spliterator.hasCharacteristics(Spliterator.NONNULL)),
          () -> assertFalse(spliterator.hasCharacteristics(Spliterator.IMMUTABLE)),
          () -> assertFalse(spliterator.hasCharacteristics(Spliterator.CONCURRENT)),
          () -> assertFalse(spliterator.hasCharacteristics(Spliterator.SORTED)),
          () -> assertFalse(spliterator.hasCharacteristics(Spliterator.SIZED)),
          () -> assertFalse(spliterator.hasCharacteristics(Spliterator.SUBSIZED))
      );
    }
  }


  @Nested
  public class Q7 {

    @Test
    public void testParallelStreamIsReallyParallel() {
      var graph = Graph.of(128, "");
      IntStream.range(0, 128).forEach(i -> graph.addEdge(7, i, "" + i));
      var edgeStream = graph.edgeStream(7);

      var threads = new CopyOnWriteArrayList<Thread>();
      var edges = edgeStream.parallel()
          .peek(_ -> threads.add(Thread.currentThread()))
          .toList();

      assertEquals(128, edges.size());
      assertTrue(threads.size() > 1);
    }

    @Test
    public void testTrySplitWithSingleEdge() {
      var graph = Graph.of(3, "default");
      graph.addEdge(0, 0, "loop");

      var edgeStream = graph.edgeStream(0);
      var spliterator = edgeStream.spliterator();
      assertNotNull(spliterator);

      spliterator.tryAdvance(e -> assertEquals("loop", e.weight()));

      spliterator.forEachRemaining(_ -> fail());
    }

    @Test
    public void testTrySplitWithNoEdges() {
      var graph = Graph.of(5, "default");
      var edgeStream = graph.edgeStream(0);
      var spliterator = edgeStream.spliterator();

      assertFalse(spliterator.tryAdvance(_ -> fail()));
    }

    @Test
    public void testTrySplitWithConsecutiveEdges() {
      var graph = Graph.of(6, (String) null);
      graph.addEdge(0, 1, "A");
      graph.addEdge(0, 2, "B");
      graph.addEdge(0, 3, "C");
      graph.addEdge(0, 4, "D");
      graph.addEdge(0, 5, "E");

      var stream = graph.edgeStream(0);
      var spliterator = stream.spliterator();

      var leftSplit = spliterator.trySplit();
      assertNotNull(leftSplit);

      var leftData = new ArrayList<String>();
      leftSplit.forEachRemaining(e -> leftData.add(e.weight()));

      var rightData = new ArrayList<String>();
      spliterator.forEachRemaining(e -> rightData.add(e.weight()));

      assertEquals(List.of("A", "B"), leftData);
      assertEquals(List.of("C", "D", "E"), rightData);
    }

    @Test
    public void testTrySplitSparseGraph() {
      var graph = Graph.of(10, (String) null);
      graph.addEdge(0, 1, "A");
      graph.addEdge(0, 5, "B");
      graph.addEdge(0, 8, "C");
      graph.addEdge(0, 9, "D");

      var stream = graph.edgeStream(0);
      var spliterator = stream.spliterator();

      var leftSplit = spliterator.trySplit();
      assertNotNull(leftSplit);

      var leftData = new ArrayList<String>();
      leftSplit.forEachRemaining(e -> leftData.add(e.weight()));

      var rightData = new ArrayList<String>();
      spliterator.forEachRemaining(e -> rightData.add(e.weight()));

      assertEquals(List.of("A"), leftData);
      assertEquals(List.of("B", "C", "D"), rightData);
    }

    @Test
    public void testTrySplitEdges() {
      var graph = Graph.of(10, "default");
      graph.addEdge(0, 2, "weight2");
      graph.addEdge(0, 4, "weight4");
      graph.addEdge(0, 6, "weight6");
      graph.addEdge(0, 8, "weight8");
      graph.addEdge(0, 9, "weight9");

      var edgeStream = graph.edgeStream(0);
      var spliterator = edgeStream.spliterator();

      var leftSpit = spliterator.trySplit();
      assertNotNull(leftSpit);

      var rightList = new ArrayList<Graph.Edge<String>>();
      spliterator.forEachRemaining(rightList::add);

      var leftList = new ArrayList<Graph.Edge<String>>();
      leftSpit.forEachRemaining(leftList::add);

      assertEquals(3, rightList.size());
      assertEquals(new Graph.Edge<>(0, 6, "weight6"), rightList.get(0));
      assertEquals(new Graph.Edge<>(0, 8, "weight8"), rightList.get(1));
      assertEquals(new Graph.Edge<>(0, 9, "weight9"), rightList.get(2));

      assertEquals(2, leftList.size());
      assertEquals(new Graph.Edge<>(0, 2, "weight2"), leftList.get(0));
      assertEquals(new Graph.Edge<>(0, 4, "weight4"), leftList.get(1));
    }

    @Test
    public void testTrySplitUpdatesSizesCorrectly() {
      var graph = Graph.of(10, "default");
      graph.addEdge(0, 2, "weight2");
      graph.addEdge(0, 4, "weight4");
      graph.addEdge(0, 6, "weight6");
      graph.addEdge(0, 8, "weight8");

      var edgeStream = graph.edgeStream(0);
      var spliterator = edgeStream.spliterator();
      assertNotNull(spliterator);

      var originalEstimate = spliterator.estimateSize();
      spliterator.trySplit();
      var newEstimate = spliterator.estimateSize();

      assertTrue(newEstimate < originalEstimate);
    }

    @Test
    public void testTrySplitMultipleTimes() {
      var graph = Graph.of(10_000, (String) null);
      for (int i = 1; i < 1_000; i++) {
        graph.addEdge(0, i * 10, "edge" + i * 10);
      }

      var stream = graph.edgeStream(0);
      var spliterator = stream.spliterator();

      var firstSplit = spliterator.trySplit();
      assertNotNull(firstSplit);

      var secondSplit = spliterator.trySplit();
      assertNotNull(secondSplit);

      var thirdSplit = spliterator.trySplit();
      assertNotNull(thirdSplit);

      // Collect all edges from all spliterators
      var allEdges = new ArrayList<Graph.Edge<String>>();
      firstSplit.forEachRemaining(allEdges::add);
      secondSplit.forEachRemaining(allEdges::add);
      thirdSplit.forEachRemaining(allEdges::add);
      spliterator.forEachRemaining(allEdges::add);

      assertEquals(graph.edgeStream(0).toList(), allEdges);
    }

    @Test
    public void testTrySplitPreservesCharacteristics() {
      var graph = Graph.of(5, null);
      graph.addEdge(0, 1, "A");
      graph.addEdge(0, 2, "B");
      graph.addEdge(0, 3, "C");
      graph.addEdge(0, 4, "D");

      var stream = graph.edgeStream(0);
      var spliterator = stream.spliterator();
      var characteristics = spliterator.characteristics();

      var leftSplit = spliterator.trySplit();

      assertNotNull(leftSplit);
      assertEquals(characteristics, leftSplit.characteristics());
      assertEquals(characteristics, spliterator.characteristics());
    }
  }
}