package vec;

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

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

import module java.base;

public class VecTest {

  @Nested
  public class Q1 {
    @Test
    public void testConstructorOfStrings() {
      var vec = new Vec<>("A", "B", "C");
      assertEquals("A, B, C", vec.toString());
    }

    @Test
    public void testConstructorOfIntegers() {
      var vec = new Vec<>(1, 2, 3);
      assertEquals("1, 2, 3", vec.toString());
    }

    @Test
    public void testConstructorEmpty() {
      var vec = new Vec<>();
      assertEquals("", vec.toString());
    }

    @Test
    public void testConstructorOneValue() {
      var vec = new Vec<>("one value");
      assertEquals("one value", vec.toString());
    }

    @Test
    public void testConstructorDuplicates() {
      var vec = new Vec<>("A", "A");
      assertEquals("A, A", vec.toString());
    }

    @Test
    public void testConstructorRespectEncapsulation() {
      var array = new Object[] {"one", "two", "three"};
      var vec = new Vec<>(array);
      array[1] = "boom";
      assertEquals("one, two, three", vec.toString());
    }

    @Test
    public void testConstructorWithNull() {
      assertThrows(NullPointerException.class, () -> new Vec<>(null));
    }

    @Test
    public void testConstructorWithNAnElementAndull() {
      assertThrows(NullPointerException.class, () -> new Vec<>("element", null));
    }

    @Test
    public void testOnePublicConstructor() {
      assertEquals(1, Vec.class.getConstructors().length);
    }

    @Test
    public void testOnlyTwoFields() {
      var fields = Arrays.stream(Vec.class.getDeclaredFields())
          .filter(field -> !field.accessFlags().contains(AccessFlag.STATIC))
          .toList();
      assertEquals(2, fields.size(), "Vec should have exactly two instance fields");
    }

    @Test
    public void testClassIsFinal() {
      assertTrue(Vec.class.accessFlags().contains(AccessFlag.FINAL));
    }

    @Test
    public void testClassConstructorsCallSuperAsLastInstruction() throws IOException {
      var className = "/" + Vec.class.getName().replace('.', '/') + ".class";
      byte[] data;
      try (var input = Vec.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())
        );
      }
    }
  }


  @Nested
  public class Q2 {
    @Test
    public void checkToStringUsesStream() throws Exception {
      var classFile = ClassFile.of();
      var path = Vec.class.getName().replace('.', '/') + ".class";
      var stream = getClass().getClassLoader().getResourceAsStream(path);
      assertNotNull(stream);

      var bytes = stream.readAllBytes();
      var classModel = classFile.parse(bytes);

      var usesStream = false;
      for (var method : classModel.methods()) {
        if (method.methodName().equalsString("toString")) {
          var code = method.code().orElseThrow();
          // Look for Invoke instructions pointing to java.util.stream.Stream
          for (var element : code) {
            if (element instanceof InvokeInstruction invokeInstruction) {
              if (invokeInstruction.owner().asInternalName().contains("java/util/stream")) {
                usesStream = true;
                break;
              }
            }
          }
        }
      }
      assertTrue(usesStream, "toString() should be implemented using a Stream");
    }
  }


  @Nested
  public class Q3 {
    @Test
    public void testAddInTheMiddle() {
      var vec = new Vec<>("A", "C");
      vec.add(1, "B");
      assertEquals("A, B, C", vec.toString());
    }

    @Test
    public void testAddInFront() {
      var vec = new Vec<>("A", "C");
      vec.add(0, "B");
      assertEquals("B, A, C", vec.toString());
    }

    @Test
    public void testAddInTheEnd() {
      var vec = new Vec<>("A", "C");
      vec.add(2, "B");
      assertEquals("A, C, B", vec.toString());
    }

    @Test
    public void testAddIntegers() {
      var vec = new Vec<>(0, 1);
      vec.add(2, 2);
      vec.add(3, 3);
      assertEquals("0, 1, 2, 3", vec.toString());
    }

    @Test
    public void testAddNull() {
      var vec = new Vec<>();
      assertThrows(NullPointerException.class, () -> vec.add(0, null));
    }

    @Test
    public void testAddNullInTheMiddle() {
      var vec = new Vec<>("foo", "bar", "baz");
      assertThrows(NullPointerException.class, () -> vec.add(1, null));
    }

    @Test
    public void testAddNullAtTheEnd() {
      var vec = new Vec<>("foo", "bar", "baz");
      assertThrows(NullPointerException.class, () -> vec.add(3, null));
    }

    @Test
    public void testAddOutOfBounds() {
      var vec = new Vec<>("a");
      assertThrows(IndexOutOfBoundsException.class, () -> vec.add(-1, "b"));
    }

    @Test
    public void testAddOutOfBounds2() {
      var vec = new Vec<>("a");
      assertThrows(IndexOutOfBoundsException.class, () -> vec.add(2, "b"));
    }
  }


  @Nested
  public class Q4 {
    @Test
    public void testEmptyAddInTheEnd() {
      var vec = new Vec<>();
      vec.add(0, "First");
      assertEquals("First", vec.toString());
    }

    @Test
    public void testMoreResize() {
      var vec = new Vec<>(0, 1);
      for(var i = 2; i < 10; i++) {
        vec.add(i, i);
      }
      assertEquals("0, 1, 2, 3, 4, 5, 6, 7, 8, 9", vec.toString());
    }

    @Test
    public void testMoreResizeInReverse() {
      var vec = new Vec<>();
      for(var i = 0; i < 10; i++) {
        vec.add(0, i);
      }
      assertEquals("9, 8, 7, 6, 5, 4, 3, 2, 1, 0", vec.toString());
    }

    @Test
    public void testMoreResizeWithLargeValues() {
      var vec = new Vec<>();
      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for(var i = 0; i < 1_000_000; i++) {
          vec.add(i, i);
        }
      });
      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        assertEquals(7_888_888, vec.toString().length());
      });
    }
  }


  @Nested
  public class Q5 {
    @Test
    public void testSpliterator() {
      var vec = new Vec<>("1", "2");
      var spliterator = vec.spliterator();
      var result = new ArrayList<String>();
      spliterator.forEachRemaining(result::add);
      assertEquals(List.of("1", "2"), result);
    }

    @Test
    public void testEmptySpliterator() {
      var vec = new Vec<>();
      var spliterator = vec.spliterator();
      var result = new ArrayList<>();
      spliterator.forEachRemaining(result::add);
      assertEquals(List.of(), result);
    }

    @Test
    public void testSpliteratorCharacteristics() {
      var vec = new Vec<>();
      var spliterator = vec.spliterator();
      assertTrue(spliterator.hasCharacteristics(Spliterator.NONNULL));
      assertFalse(spliterator.hasCharacteristics(Spliterator.CONCURRENT));
    }

    @Test
    public void testStreamFindFirst() {
      var vec = new Vec<>(1, 5, 7, 8, 9);
      var result = vec.stream().filter(v -> v % 2 == 0).findFirst().orElseThrow();
      assertEquals(8, result);
    }

    @Test
    public void testStreamFindFirstNotFound() {
      var vec = new Vec<>(1, 5, 7, 9);
      var optional = vec.stream().filter(v -> v % 2 == 0).findFirst();
      assertTrue(optional.isEmpty());
    }

    @Test
    public void testStream() {
      var vec = new Vec<>("1", "2");
      var result = vec.stream().toList();
      assertEquals(List.of("1", "2"), result);
    }

    @Test
    public void testEmptyStream() {
      var vec = new Vec<>();
      var result = vec.stream().toList();
      assertEquals(List.of(), result);
    }

    @Test
    public void testStreamIsNotParallel() {
      var vec = new Vec<>();
      assertFalse(vec.stream().isParallel());
    }

    @Test
    public void testStreamMapCount() {
      var vec = new Vec<>("1", "2");
      var result = vec.stream().map(_ -> fail()).count();
      assertEquals(2, result);
    }

    @Test
    public void testStreamLargeValues() {
      var vec = new Vec<>();
      for(var i = 0; i < 1_000_000; i++) {
        vec.add(i, i);
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        var spliterator = vec.spliterator();
        for(var i = 0; i < 1_000_000; i++) {
          var expected = i;
          spliterator.tryAdvance(v -> assertEquals(expected, v));
        }
        assertFalse(spliterator.tryAdvance(_ -> fail()));
      });
    }

    @Test
    public void testSpliteratorPerformance() {
      var vec = new Vec<>();
      for(var i = 0; i < 1_000_000; i++) {
        vec.add(i, i);
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for(var i = 0; i < 1_000_000; i++) {
          vec.spliterator().tryAdvance(_ -> {});
        }
      });
    }

    @Test
    public void testStreamPerformance() {
      var vec = new Vec<>();
      for(var i = 0; i < 1_000_000; i++) {
        vec.add(i, i);
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for(var i = 0; i < 1_000_000; i++) {
          assertEquals(1_000_000, vec.stream().count());
        }
      });
    }
  }


  @Nested
  public class Q6 {
    @Test
    public void testConcurrentModificationInSpliterator() {
      var vec = new Vec<>("A", "B");
      var spliterator = vec.spliterator();
      vec.add(2, "C");
      assertThrows(ConcurrentModificationException.class, () -> spliterator.forEachRemaining(_ -> {}));
    }

    @Test
    public void testConcurrentModificationInSpliterator2() {
      var vec = new Vec<>("A", "B");
      var spliterator = vec.spliterator();
      vec.add(2, "C");
      assertThrows(ConcurrentModificationException.class, () -> spliterator.tryAdvance(_ -> {}));
    }
  }


  @Nested
  public class Q7 {
    @Test
    public void testAsListSize() {
      var vec = new Vec<>("A", "B");
      List<String> view = vec.asList();

      assertEquals(2, view.size());
    }

    @Test
    public void testAsListGet() {
      var vec = new Vec<>(7, 13);
      List<Integer> view = vec.asList();

      assertEquals(7, view.get(0));
      assertEquals(13, view.get(1));
    }

    @Test
    public void testAsListEquals() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();

      assertEquals(List.of("A", "B"), view);
      assertEquals(view, List.of("A", "B"));
    }

    @Test
    public void testAsListHashCode() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();

      assertEquals(List.of("A", "B").hashCode(), view.hashCode());
    }

    @Test
    public void testAsListContains() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();

      assertTrue(view.contains("A"));
      assertTrue(view.contains("B"));
      assertFalse(view.contains("C"));
    }

    @Test
    public void testAsListIndexOf() {
      var vec = new Vec<>("A", "B", "A");
      var view = vec.asList();

      assertEquals(0, view.indexOf("A"));
      assertEquals(1, view.indexOf("B"));
      assertEquals(-1, view.indexOf("C"));
    }

    @Test
    public void testAsListLastIndexOf() {
      var vec = new Vec<>("A", "B", "A");
      var view = vec.asList();

      assertEquals(2, view.lastIndexOf("A"));
      assertEquals(1, view.lastIndexOf("B"));
      assertEquals(-1, view.lastIndexOf("C"));
    }

    @Test
    public void testAsListToArrayObject() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();

      assertArrayEquals(new Object[]{"A", "B"}, view.toArray());
    }

    @Test
    public void testAsListToArray() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();

      assertArrayEquals(new String[]{"A", "B"}, view.toArray(new String[0]));
    }

    @Test
    public void testAsListToArrayMethodReference() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();

      assertArrayEquals(new String[]{"A", "B"}, view.toArray(String[]::new));
    }

    @Test
    public void testAsListSizePerformance() {
      var vec = new Vec<>();
      for(var i = 0; i < 1_000_000; i++) {
        vec.add(i, i);
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for(var i = 0; i < 1_000_000; i++) {
          var view = vec.asList();
          assertEquals(1_000_000, view.size());
        }
      });
    }

    @Test
    public void testAsListGetPerformance() {
      var vec = new Vec<>();
      for(var i = 0; i < 1_000_000; i++) {
        vec.add(i, i);
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for(var i = 0; i < 1_000_000; i++) {
          var view = vec.asList();
          assertEquals(i, view.get(i));
        }
      });
    }

    @Test
    public void testAsListGetOutOfBounds() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();

      assertThrows(IndexOutOfBoundsException.class, () -> view.get(-1));
      assertThrows(IndexOutOfBoundsException.class, () -> view.get(2));
    }
  }


  @Nested
  public class Q8 {
    @Test
    public void testAfterMutationGetThrowsCCE() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      vec.add(2, "C");
      assertThrows(ConcurrentModificationException.class, () -> view.get(1));
    }

    @Test
    public void testAfterMutationGetFirstThrowsCCE() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      vec.add(2, "C");
      assertThrows(ConcurrentModificationException.class, view::getFirst);
    }

    @Test
    public void testAfterMutationContainsThrowsCCE() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      vec.add(2, "C");
      assertThrows(ConcurrentModificationException.class, () -> view.contains("A"));
    }

    @Test
    public void testAfterMutationIteratorThrowsCCE() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      vec.add(2, "C");
      var it = view.iterator();
      assertTrue(it.hasNext());
      assertThrows(ConcurrentModificationException.class, it::next);
    }

    @Test
    public void testAfterMutationSizeDoesNotThrowCCE() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      vec.add(2, "C");
      assertEquals(2, view.size());
    }
  }


  @Nested
  public class Q9 {
    @Test
    public void testAsListStream() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      assertEquals(List.of("A", "B"), view.stream().toList());
    }

    @Test
    public void testEmptyAsListStream() {
      var vec = new Vec<>();
      var view = vec.asList();
      assertEquals(List.of(), view.stream().toList());
    }

    @Test
    public void testStreamPerformance() {
      var vec = new Vec<>();
      for(var i = 0; i < 1_000_000; i++) {
        vec.add(i, i);
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for(var i = 0; i < 1_000_000; i++) {
          var view = vec.asList();
          assertEquals(1_000_000, view.stream().count());
        }
      });
    }

    @Test
    public void testAsListStreamElementsAreDeclaredNonNull() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      assertTrue(view.stream().spliterator().hasCharacteristics(Spliterator.NONNULL));
    }

    @Test
    public void testAsListStreamSourceIsDeclaredImmutable() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      assertTrue(view.stream().spliterator().hasCharacteristics(Spliterator.IMMUTABLE));
    }

    @Test
    public void testAsListStreamIsParallel() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      assertTrue(view.stream().parallel().isParallel());
    }

    @Test
    public void testAsListStreamAfterMutationCanStillReadTheNumberOfElements() {
      var vec = new Vec<>("A", "B", "D", "E");
      var view = vec.asList();
      vec.add(2, "C");
      assertEquals(4, view.stream().count());
    }

    @Test
    public void testAsListStreamAfterMutationThrowsCCE2() {
      var vec = new Vec<>("A", "B");
      var view = vec.asList();
      var stream = view.stream();
      vec.add(2, "C");
      assertThrows(ConcurrentModificationException.class, stream::toList);
    }

    @Test
    public void testAsListSpliteratorAfterMutationThrowsCCE() {
      var vec = new Vec<>("A", "B");
      var stream = vec.asList().stream();
      vec.add(2, "C");
      assertThrows(ConcurrentModificationException.class, () -> stream.spliterator().tryAdvance(_ -> {}));
    }
  }
}