package fr.uge.sed;

import static java.util.stream.Collectors.joining;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.BufferedReader;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.AccessFlag;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.IntStream;

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

public class StreamEditorTest {
  private static String convertOneLine(StreamEditor.Transformer transformer, String line) throws IOException {
    var stringReader = new StringReader(line);
    var writer = new CharArrayWriter();
    try(var reader = new BufferedReader(stringReader)) {
      StreamEditor.rewrite(reader, writer, transformer);
    }
    var result = writer.toString();
    if (result.endsWith("\n")) {
      return result.substring(0, result.length() - 1);
    }
    return result;
  }

  @Nested
  public class Q1 {

    @Test
    @DisplayName("Parse transformer for lowercase command")
    public void parseTransformerLowerCase() throws IOException {
      var transformer = StreamEditor.parseTransformer("l");

      assertEquals("hello", convertOneLine(transformer, "HELLO"));
      assertEquals("world", convertOneLine(transformer, "WORLD"));
      assertEquals("", convertOneLine(transformer, ""));
    }

    @Test
    @DisplayName("Parse transformer for replace tabs command")
    public void parseTransformerReplace() throws IOException {
      var transformer = StreamEditor.parseTransformer("r");

      assertEquals("hello world", convertOneLine(transformer, "hello\tworld"));
      assertEquals("a b c", convertOneLine(transformer, "a\tb\tc"));
      assertEquals("no tabs here", convertOneLine(transformer, "no tabs here"));
      assertEquals("", convertOneLine(transformer, ""));
    }

    @Test
    @DisplayName("Parse transformer for star zero command")
    public void parseTransformerStarZero() throws IOException {
      var transformer = StreamEditor.parseTransformer("*0");

      assertEquals("hello", convertOneLine(transformer, "*hello*"));
      assertEquals("foobar", convertOneLine(transformer, "*foo*bar*"));
      assertEquals("", convertOneLine(transformer, "*"));
      assertEquals("no stars", convertOneLine(transformer, "no stars"));
    }

    @Test
    @DisplayName("Parse transformer for star one command")
    public void parseTransformerStarOne() throws IOException {
      var transformer = StreamEditor.parseTransformer("*1");

      assertEquals("*hello*", convertOneLine(transformer, "*hello*"));
      assertEquals("*foo*bar*", convertOneLine(transformer, "*foo*bar*"));
      assertEquals("no stars", convertOneLine(transformer, "no stars"));
    }

    @Test
    @DisplayName("Parse transformer for star multiple command")
    public void parseTransformerStarMultiple() throws IOException {
      var transformer = StreamEditor.parseTransformer("*5");

      assertEquals("*****hello*****", convertOneLine(transformer, "*hello*"));
      assertEquals("*****foo*****bar*****", convertOneLine(transformer, "*foo*bar*"));
      assertEquals("no stars", convertOneLine(transformer, "no stars"));
    }

    @Test
    @DisplayName("Parse transformer for star nine command")
    public void parseTransformerStarNine() throws IOException {
      var transformer = StreamEditor.parseTransformer("*9");

      assertEquals("*********", convertOneLine(transformer, "*"));
      assertEquals("*********test*********", convertOneLine(transformer, "*test*"));
    }

    @Test
    @DisplayName("Parse transformer replace handles multiple tabs")
    public void parseTransformerReplaceMultipleTabs() throws IOException {
      var transformer = StreamEditor.parseTransformer("r");

      assertEquals("a b c d", convertOneLine(transformer, "a\tb\tc\td"));
      assertEquals("   ", convertOneLine(transformer, "\t\t\t"));
    }

    @Test
    @DisplayName("Rewrite several lines with lowercase transformation")
    public void rewriteSeveralLinesLowerCase() throws IOException {
      var transformer = StreamEditor.parseTransformer("l");
      var reader = new StringReader("""
          foo
          BaR
          BAZ
          """);
      var writer = new CharArrayWriter();
      try(var bufferedReader = new BufferedReader(reader)) {
        StreamEditor.rewrite(bufferedReader, writer, transformer);
      }
      assertEquals("""
          foo
          bar
          baz
          """, writer.toString());
    }

    @Test
    @DisplayName("Rewrite several lines with replace tabs transformation")
    public void rewriteSeveralLinesReplace() throws IOException {
      var transformer = StreamEditor.parseTransformer("r");
      var reader = new StringReader("""
          foo
          \t.
          b\ta\tz
          """);
      var writer = new CharArrayWriter();
      try(var bufferedReader = new BufferedReader(reader)) {
        StreamEditor.rewrite(bufferedReader, writer, transformer);
      }
      assertEquals("""
          foo
           .
          b a z
          """, writer.toString());
    }

    @Test
    @DisplayName("Rewrite several lines with star five transformation")
    public void rewriteSeveralLinesStarFive() throws IOException {
      var transformer = StreamEditor.parseTransformer("*5");
      var reader = new StringReader("""
          f*o
          bar
          **Z
          """);
      var writer = new CharArrayWriter();
      try(var bufferedReader = new BufferedReader(reader)) {
        StreamEditor.rewrite(bufferedReader, writer, transformer);
      }
      assertEquals("""
          f*****o
          bar
          **********Z
          """, writer.toString());
    }

    @Test
    @DisplayName("Rewrite empty input to an empty output")
    public void rewriteEmpty() throws IOException {
      var transformer = StreamEditor.parseTransformer("l");
      var reader = Reader.nullReader();
      var writer = new CharArrayWriter();
      try(var bufferedReader = new BufferedReader(reader)) {
        StreamEditor.rewrite(bufferedReader, writer, transformer);
      }
      assertEquals("", writer.toString());
    }

    @Test
    @DisplayName("Rewrite a lot of lines, is it efficient ?")
    public void rewriteALotOfLines() throws IOException {
      var transformer = StreamEditor.parseTransformer("l");
      var text = IntStream.range(0, 100_000).mapToObj(i -> i + "\n").collect(joining(""));
      var reader = new StringReader(text);
      var writer = new CharArrayWriter();
      try(var bufferedReader = new BufferedReader(reader)) {
        StreamEditor.rewrite(bufferedReader, writer, transformer);
      }
      assertEquals(text, writer.toString());
    }

    @Test
    @DisplayName("Rewrite an ungodly number of lines")
    public void rewriteAnUngodlyNumberOfLines() throws IOException {
      class HugeReader extends Reader {
        private int index;
        private boolean closed;

        @Override
        public int read(char[] cbuf, int off, int len) throws IOException {
          Objects.checkFromIndexSize(off, len, cbuf.length);
          if (closed) {
            throw new IOException("closed");
          }
          var last = Math.min(1_000_000_000, index + len);
          var toRead = Math.min(last - index, len);
          for(var i = 0; i < toRead; i++) {
            cbuf[off + i] = (index + i) % 20 == 0 ? '\n' : 'A';
          }
          if (toRead == 0) {
            return -1;
          }
          index += toRead;
          return toRead;
        }

        @Override
        public void close() {
          closed = true;
        }
      }

      var transformer = StreamEditor.parseTransformer("l");
      var reader = new HugeReader();
      var writer = Writer.nullWriter();
      try(var bufferedReader = new BufferedReader(reader)) {
        StreamEditor.rewrite(bufferedReader, writer, transformer);
      }
    }

    @Test
    @DisplayName("Parse transformer command can not be null")
    public void parseTransformerPrecondition() {
      assertThrows(NullPointerException.class, () -> StreamEditor.parseTransformer(null));
    }

    @Test
    @DisplayName("transformer with unknown commands")
    public void invalidTransformerUnknownCommands() {
      assertThrows(IllegalArgumentException.class, () -> StreamEditor.parseTransformer("z"));
      assertThrows(IllegalArgumentException.class, () -> StreamEditor.parseTransformer("\u039b\n"));
    }

    @Test
    @DisplayName("StreamEditor class is public final")
    public void streamEditorClassIsPublicFinal() {
      assertAll(
          () -> assertTrue(StreamEditor.class.accessFlags().contains(AccessFlag.FINAL)),
          () -> assertTrue(StreamEditor.class.accessFlags().contains(AccessFlag.PUBLIC))
      );
    }

    @Test
    @DisplayName("StreamEditor constructor is not visible")
    public void streamEditorConstructorIsNotVisible() {
      assertAll(
          () -> assertEquals(0, StreamEditor.class.getConstructors().length)
      );
    }

    @Test
    @DisplayName("StreamEditor transformer implementations are final")
    public void streamEditorTransformerImplementationAreFinals() {
      assertAll(
          () -> assertTrue(StreamEditor.parseTransformer("l").getClass().accessFlags().contains(AccessFlag.FINAL)),
          () -> assertTrue(StreamEditor.parseTransformer("r").getClass().accessFlags().contains(AccessFlag.FINAL)),
          () -> assertTrue(StreamEditor.parseTransformer("*7").getClass().accessFlags().contains(AccessFlag.FINAL))
      );
    }

    @Test
    @DisplayName("rewrite arguments can not be null")
    public void preconditions() {
      var transformer = StreamEditor.parseTransformer("l");
      assertAll(
          () -> assertThrows(NullPointerException.class, () -> StreamEditor.parseTransformer(null)),
          () -> assertThrows(NullPointerException.class, () -> StreamEditor.rewrite(null, Writer.nullWriter(), transformer)),
          () -> assertThrows(NullPointerException.class, () -> StreamEditor.rewrite(new BufferedReader(Reader.nullReader()), null, transformer)),
          () -> assertThrows(NullPointerException.class, () -> StreamEditor.rewrite(new BufferedReader(Reader.nullReader()), Writer.nullWriter(), null))
      );
    }

    @Test
    @DisplayName("If interface is sealed it should not have any method")
    public void ifTheInterfaceIsSealedItShouldNotHaveAnyMethod() {
      var transformerInterface = StreamEditor.parseTransformer("l").getClass().getInterfaces()[0];
      if (transformerInterface.isSealed()) {
        assertEquals(0, transformerInterface.getMethods().length);
      }
    }
  }


  @Nested
  public class Q2 {
    @Test
    @DisplayName("Create transformer lowercase handles dotless I")
    public void createTransformerLowercaseDotlessI() throws IOException {
      // see https://en.wikipedia.org/wiki/Dotless_I
      var transformer = StreamEditor.parseTransformer("l");
      var stringReader = new StringReader("I\n");
      var writer = new CharArrayWriter();

      var oldLocale = Locale.getDefault();
      Locale.setDefault(Locale.forLanguageTag("tr-tr"));
      try {
        try(var reader = new BufferedReader(stringReader)) {
          StreamEditor.rewrite(reader, writer, transformer);
        }
      } finally {
        Locale.setDefault(oldLocale);
      }
      assertEquals("i\n", writer.toString());
    }

    @Test
    @DisplayName("Create transformer lowercase handles non-Latin characters")
    public void createTransformerLowerCaseNonLatin() throws IOException {
      var transformer = StreamEditor.parseTransformer("l");
      var stringReader = new StringReader("\u039b\n");
      var writer = new CharArrayWriter();

      try(var reader = new BufferedReader(stringReader)) {
        StreamEditor.rewrite(reader, writer, transformer);
      }

      assertEquals("\u03bb\n", writer.toString());
    }
  }


  @Nested
  public class Q3 {
    @Test
    @DisplayName("If interface is not sealed it should have at least one declared method")
    public void ifTheInterfaceIsNotSealedItShouldNotHaveAtLeastOneDeclaredMethod() {
      var transformerInterface = StreamEditor.parseTransformer("l").getClass().getInterfaces()[0];
      if (!transformerInterface.isSealed()) {
        assertTrue(transformerInterface.getDeclaredMethods().length > 0);
      }
    }
  }

  @Nested
  public class Q4 {
    @Test
    @DisplayName("If implementation uses lambdas the interface should be functional")
    public void ifTheImplementationUsesALambdasTheInterfaceShouldBeFunctional() throws IOException {
      var transformerClass = StreamEditor.parseTransformer("l").getClass();
      if (transformerClass.isHidden()) {
        var transformerInterface = transformerClass.getInterfaces()[0];
        assertTrue(transformerInterface.isAnnotationPresent(FunctionalInterface.class));
      }
    }
  }

  @Nested
  public class Q6 {
    @Test
    @DisplayName("Character iterator with basic string")
    public void characterIteratorBasicString() {
      var iterator = StreamEditor.characterIterator("bar");

      assertTrue(iterator.hasNext());
      assertEquals('b', iterator.next());
      assertTrue(iterator.hasNext());
      assertEquals('a', iterator.next());
      assertTrue(iterator.hasNext());
      assertEquals('r', iterator.next());
      assertFalse(iterator.hasNext());
    }

    @Test
    public void characterIteratorEmptyString() {
      var iterator = StreamEditor.characterIterator("");
      assertFalse(iterator.hasNext());
    }

    @Test
    @DisplayName("Character iterator with empty string")
    public void characterIteratorSingleCharacter() {
      var iterator = StreamEditor.characterIterator("x");

      assertTrue(iterator.hasNext());
      assertEquals('x', iterator.next());
      assertFalse(iterator.hasNext());
    }

    @Test
    @DisplayName("Character iterator with single character")
    public void characterIteratorWithSpecialCharacters() {
      var iterator = StreamEditor.characterIterator("a\tb*2");

      assertTrue(iterator.hasNext());
      assertEquals('a', iterator.next());
      assertTrue(iterator.hasNext());
      assertEquals('\t', iterator.next());
      assertTrue(iterator.hasNext());
      assertEquals('b', iterator.next());
      assertTrue(iterator.hasNext());
      assertEquals('*', iterator.next());
      assertTrue(iterator.hasNext());
      assertEquals('2', iterator.next());
      assertFalse(iterator.hasNext());
    }

    @Test
    @DisplayName("Character iterator with special characters")
    public void characterIteratorWithNonLatinCharacter() {
      var iterator = StreamEditor.characterIterator("\u039b");

      assertTrue(iterator.hasNext());
      assertEquals('\u039b', iterator.next());
      assertFalse(iterator.hasNext());
    }

    @Test
    @DisplayName("Character iterators on the same input have the same output")
    public void characterIteratorsHaveSameOutput() {
      var iterator1 = StreamEditor.characterIterator("abc");
      var iterator2 = StreamEditor.characterIterator("abc");

      assertEquals('a', iterator1.next());
      assertEquals('a', iterator2.next());
      assertEquals('b', iterator1.next());
      assertEquals('b', iterator2.next());
    }

    @Test
    @DisplayName("Character iterator with long string")
    public void characterIteratorLongString() {
      var longString = "a".repeat(1_000_000);
      var iterator = StreamEditor.characterIterator(longString);

      var count = 0;
      while (iterator.hasNext()) {
        assertEquals('a', iterator.next());
        count++;
      }
      assertEquals(1_000_000, count);
    }

    @Test
    @DisplayName("Character iterator throws NSEE when empty")
    public void characterIteratorThrowsNSEEWhenEmpty() {
      var iterator = StreamEditor.characterIterator("");

      assertThrows(NoSuchElementException.class, iterator::next);
    }

    @Test
    @DisplayName("Character iterator throws NSEE after exhaustion")
    public void characterIteratorThrowsNSEEAfterExhaustion() {
      var iterator = StreamEditor.characterIterator("a");
      iterator.next();

      assertThrows(NoSuchElementException.class, iterator::next);
    }

    @Test
    @DisplayName("Character iterator throws UnsupportedOperationException when calling remove")
    public void characterIteratorThrowsUOEWhenCallingRemove() {
      var iterator = StreamEditor.characterIterator("a");
      iterator.next();

      assertThrows(UnsupportedOperationException.class, iterator::remove);
    }
  }


  @Nested
  public class Q7 {
    @Test
    @DisplayName("Parse one transformer for lowercase")
    public void parseOneTransformerLowerCase() throws IOException {
      var iterator = StreamEditor.characterIterator("l");
      var transformer = StreamEditor.parseOneTransformer(iterator);

      assertEquals("hello", convertOneLine(transformer, "HELLO"));
      assertEquals("world", convertOneLine(transformer, "WORLD"));
      assertEquals("", convertOneLine(transformer, ""));
    }

    @Test
    @DisplayName("Parse one transformer for replace tabs")
    public void parseOneTransformerReplace() throws IOException {
      var iterator = StreamEditor.characterIterator("r");
      var transformer = StreamEditor.parseOneTransformer(iterator);

      assertEquals("hello world", convertOneLine(transformer, "hello\tworld"));
      assertEquals("a b c", convertOneLine(transformer, "a\tb\tc"));
      assertEquals("no tabs here", convertOneLine(transformer, "no tabs here"));
      assertEquals("", convertOneLine(transformer, ""));
    }

    @Test
    @DisplayName("Parse one transformer for zero star")
    public void parseOneTransformerStarZero() throws IOException {
      var iterator = StreamEditor.characterIterator("*0");
      var transformer = StreamEditor.parseOneTransformer(iterator);

      assertEquals("hello", convertOneLine(transformer, "*hello*"));
      assertEquals("foobar", convertOneLine(transformer, "*foo*bar*"));
      assertEquals("", convertOneLine(transformer, "*"));
      assertEquals("no stars", convertOneLine(transformer, "no stars"));
    }

    @Test
    @DisplayName("Parse one transformer for one star")
    public void parseOneTransformerStarOne() throws IOException {
      var iterator = StreamEditor.characterIterator("*1");
      var transformer = StreamEditor.parseOneTransformer(iterator);

      assertEquals("*hello*", convertOneLine(transformer, "*hello*"));
      assertEquals("*foo*bar*", convertOneLine(transformer, "*foo*bar*"));
      assertEquals("no stars", convertOneLine(transformer, "no stars"));
    }

    @Test
    @DisplayName("Parse one transformer for multiple stars")
    public void parseOneTransformerStarMultiple() throws IOException {
      var iterator = StreamEditor.characterIterator("*5");
      var transformer = StreamEditor.parseOneTransformer(iterator);

      assertEquals("*****hello*****", convertOneLine(transformer, "*hello*"));
      assertEquals("*****foo*****bar*****", convertOneLine(transformer, "*foo*bar*"));
      assertEquals("no stars", convertOneLine(transformer, "no stars"));
    }

    @Test
    @DisplayName("Parse one transformer for nine stars")
    public void parseOneTransformerStarNine() throws IOException {
      var iterator = StreamEditor.characterIterator("*9");
      var transformer = StreamEditor.parseOneTransformer(iterator);

      assertEquals("*********", convertOneLine(transformer, "*"));
      assertEquals("*********test*********", convertOneLine(transformer, "*test*"));
    }

    @Test
    @DisplayName("Parse one transformer replace multiple tabs")
    public void parseOneTransformerReplaceMultipleTabs() throws IOException {
      var iterator = StreamEditor.characterIterator("r");
      var transformer = StreamEditor.parseOneTransformer(iterator);

      assertEquals("a b c d", convertOneLine(transformer, "a\tb\tc\td"));
      assertEquals("   ", convertOneLine(transformer, "\t\t\t"));
    }

    @Test
    @DisplayName("Parse one transformer consumes correct number of characters")
    public void parseOneTransformerConsumesCorrectNumberOfCharacters() {
      // Test that 'l' consumes only one character
      var iterator1 = StreamEditor.characterIterator("lx");
      StreamEditor.parseOneTransformer(iterator1);
      assertTrue(iterator1.hasNext());
      assertEquals('x', iterator1.next());

      // Test that 'r' consumes only one character
      var iterator2 = StreamEditor.characterIterator("ry");
      StreamEditor.parseOneTransformer(iterator2);
      assertTrue(iterator2.hasNext());
      assertEquals('y', iterator2.next());

      // Test that '*' consumes two characters
      var iterator3 = StreamEditor.characterIterator("*5z");
      StreamEditor.parseOneTransformer(iterator3);
      assertTrue(iterator3.hasNext());
      assertEquals('z', iterator3.next());
    }

    @Test
    @DisplayName("Parse one transformer with invalid command")
    public void parseOneTransformerInvalidCommand() {
      var iterator = StreamEditor.characterIterator("x");

      assertThrows(IllegalArgumentException.class, () -> StreamEditor.parseOneTransformer(iterator));
    }

    @Test
    @DisplayName("Parse one transformer invalid command message")
    public void parseOneTransformerInvalidCommandMessage() {
      var iterator = StreamEditor.characterIterator("z");
      var exception = assertThrows(IllegalArgumentException.class,
          () -> StreamEditor.parseOneTransformer(iterator));
      assertFalse(exception.getMessage().isEmpty());
    }

    @Test
    @DisplayName("Parse one transformer with empty iterator")
    public void parseOneTransformerEmptyIterator() {
      var iterator = Collections.<Character>emptyIterator();

      assertThrows(NoSuchElementException.class, () -> StreamEditor.parseOneTransformer(iterator));
    }

    @Test
    @DisplayName("Parse one transformer lowercase with locale")
    public void parseOneTransformerLowerCaseWithLocale() throws IOException {
      var iterator = StreamEditor.characterIterator("l");
      var transformer = StreamEditor.parseOneTransformer(iterator);

      // Test that it uses ROOT locale (not affected by default locale)
      var oldLocale = Locale.getDefault();
      Locale.setDefault(Locale.forLanguageTag("tr-tr"));
      try {
        assertEquals("i", convertOneLine(transformer, "I")); // Should be 'i', not 'ı' (dotless i)
      } finally {
        Locale.setDefault(oldLocale);
      }
    }
  }

  @Nested
  public class Q8 {
    @Test
    @DisplayName("Parse all transformers with empty iterator")
    public void parseAllTransformersEmptyIterator() {
      var iterator = Collections.<Character>emptyIterator();
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertTrue(transformers.isEmpty());
    }

    @Test
    @DisplayName("Parse all transformers with single command")
    public void parseAllTransformersSingleCommand() throws IOException {
      var iterator = StreamEditor.characterIterator("l");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(1, transformers.size());
      assertEquals("hello", convertOneLine(transformers.get(0), "HELLO"));
    }

    @Test
    @DisplayName("Parse all transformers with multiple simple commands")
    public void parseAllTransformersMultipleSimpleCommands() throws IOException {
      var iterator = StreamEditor.characterIterator("lrl");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(3, transformers.size());
      assertEquals("hello", convertOneLine(transformers.get(0), "HELLO"));
      assertEquals("a b", convertOneLine(transformers.get(1), "a\tb"));
      assertEquals("world", convertOneLine(transformers.get(2), "WORLD"));
    }

    @Test
    @DisplayName("Parse all transformers with star commands")
    public void parseAllTransformersWithStarCommands() throws IOException {
      var iterator = StreamEditor.characterIterator("*2*5");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(2, transformers.size());
      assertEquals("**hello**", convertOneLine(transformers.get(0), "*hello*"));
      assertEquals("*****test*****", convertOneLine(transformers.get(1), "*test*"));
    }

    @Test
    @DisplayName("Parse all transformers with mixed commands")
    public void parseAllTransformersMixedCommands() throws IOException {
      var iterator = StreamEditor.characterIterator("l*3r");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(3, transformers.size());
      assertEquals("hello", convertOneLine(transformers.get(0), "HELLO"));
      assertEquals("***test***", convertOneLine(transformers.get(1), "*test*"));
      assertEquals("a b", convertOneLine(transformers.get(2), "a\tb"));
    }

    @Test
    @DisplayName("Parse all transformers with a character repetition")
    public void parseAllTransformersCharacterRepetition() throws IOException {
      var iterator = StreamEditor.characterIterator("lrlrlr");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(6, transformers.size());
      for (var i = 0; i < 6; i++) {
        if (i % 2 == 0) {
          // Even indices should be lowercase transformers
          assertEquals("test", convertOneLine(transformers.get(i), "TEST"));
        } else {
          // Odd indices should be replace transformers
          assertEquals("a b", convertOneLine(transformers.get(i), "a\tb"));
        }
      }
    }

    @Test
    @DisplayName("Parse all transformers with complex star sequence")
    public void parseAllTransformersComplexStarSequence() throws IOException {
      var iterator = StreamEditor.characterIterator("*0*1*9");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(3, transformers.size());
      assertEquals("hello", convertOneLine(transformers.get(0), "*hello*")); // *0 removes stars
      assertEquals("*hello*", convertOneLine(transformers.get(1), "*hello*")); // *1 keeps stars
      assertEquals(
          "*********hello*********", convertOneLine(transformers.get(2), "*hello*")); // *9 multiplies
    }

    @Test
    @DisplayName("Parse all transformers consumes entire iterator")
    public void parseAllTransformersConsumesEntireIterator() {
      var iterator = StreamEditor.characterIterator("lr*5");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(3, transformers.size());
      assertFalse(iterator.hasNext()); // Iterator should be exhausted
    }

    @Test
    @DisplayName("Parse all transformers with invalid command")
    public void parseAllTransformersWithInvalidCommand() {
      var iterator = StreamEditor.characterIterator("lxr");

      assertThrows(
          IllegalArgumentException.class, () -> StreamEditor.parseAllTransformers(iterator));
    }

    @Test
    @DisplayName("Parse all transformers with incomplete star command")
    public void parseAllTransformersIncompleteStarCommand() {
      var iterator = StreamEditor.characterIterator("l*");

      assertThrows(NoSuchElementException.class, () -> StreamEditor.parseAllTransformers(iterator));
    }

    @Test
    @DisplayName("Parse all transformers with all digits for star")
    public void parseAllTransformersAllDigitsForStar() throws IOException {
      var iterator = StreamEditor.characterIterator("*0*1*2*3*4*5*6*7*8*9");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(10, transformers.size());

      for (var i = 0; i < 10; i++) {
        var expected = "*".repeat(i);
        var result = convertOneLine(transformers.get(i), "*");
        assertEquals(expected, result);
      }
    }

    @Test
    @DisplayName("Parse all transformers with a long sequence")
    public void parseAllTransformersLongSequence() throws IOException {
      var commandString = "l".repeat(10_000);
      var iterator = StreamEditor.characterIterator(commandString);
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(10_000, transformers.size());

      assertEquals("test", convertOneLine(transformers.get(0), "TEST"));
      assertEquals("test", convertOneLine(transformers.get(500), "TEST"));
      assertEquals("test", convertOneLine(transformers.get(9_999), "TEST"));
    }

    @Test
    @DisplayName("Parse all transformers with null iterator")
    public void parseAllTransformersNullIterator() {
      assertThrows(NullPointerException.class, () -> StreamEditor.parseAllTransformers(null));
    }

    @Test
    @DisplayName("Parse all transformers with empty string")
    public void parseAllTransformersEmptyString() {
      var iterator = StreamEditor.characterIterator("");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertTrue(transformers.isEmpty());
    }

    @Test
    @DisplayName("Parse all transformers preserves order")
    public void parseAllTransformersPreservesOrder() throws IOException {
      var iterator = StreamEditor.characterIterator("l*2r*0");
      var transformers = StreamEditor.parseAllTransformers(iterator);

      assertEquals(4, transformers.size());

      var input = "HELLO\tWORLD*TEST*";

      // First: lowercase
      var result1 = convertOneLine(transformers.get(0), input);
      assertEquals("hello\tworld*test*", result1);

      // Second: *2
      var result2 = convertOneLine(transformers.get(1), result1);
      assertEquals("hello\tworld**test**", result2);

      // Third: replace tabs
      var result3 = convertOneLine(transformers.get(2), result2);
      assertEquals("hello world**test**", result3);

      // Fourth: *0 (remove stars)
      var result4 = convertOneLine(transformers.get(3), result3);
      assertEquals("hello worldtest", result4);
    }
  }


  @Nested
  public class Q9 {
    @Test
    @DisplayName("Create one transformer with empty list")
    public void createOneTransformerEmptyList() throws IOException {
      var transformer = StreamEditor.createOneTransformer(List.of());

      assertEquals("hello", convertOneLine(transformer, "hello"));
      assertEquals("WORLD", convertOneLine(transformer, "WORLD"));
      assertEquals("", convertOneLine(transformer, ""));
      assertEquals("test\tline", convertOneLine(transformer, "test\tline"));
    }

    @Test
    @DisplayName("Create one transformer with single transformer")
    public void createOneTransformerSingleTransformer() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("l"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      assertEquals("hello", convertOneLine(transformer, "HELLO"));
      assertEquals("world", convertOneLine(transformer, "WORLD"));
      assertEquals("", convertOneLine(transformer, ""));
    }

    @Test
    @DisplayName("Create one transformer with two transformers")
    public void createOneTransformerTwoTransformers() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("lr"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      // First lowercase, then replace tabs
      assertEquals("hello world", convertOneLine(transformer, "HELLO\tWORLD"));
      assertEquals("test line", convertOneLine(transformer, "TEST\tLINE"));
      assertEquals("", convertOneLine(transformer, ""));
    }

    @Test
    @DisplayName("Create one transformer with three transformers")
    public void createOneTransformerThreeTransformers() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("l*2r"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      // First lowercase, then *2, then replace tabs
      assertEquals("**hello** world", convertOneLine(transformer, "*HELLO*\tWORLD"));
      assertEquals("test line", convertOneLine(transformer, "TEST\tLINE"));
    }

    @Test
    @DisplayName("Create one transformer with star transformations")
    public void createOneTransformerStarTransformations() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("*2*3"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      // First *2 (one star becomes two), then *3 (one star becomes three)
      // So "**" from first transformation becomes "******" from second
      assertEquals("******hello******", convertOneLine(transformer, "*hello*"));
      assertEquals("************", convertOneLine(transformer, "**"));
    }

    @Test
    @DisplayName("Create one transformer with complex sequence")
    public void createOneTransformerComplexSequence() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("*0lr*5"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      // *0 removes stars, l lowercases, r replaces tabs, *5 makes 5 stars
      var input = "*HELLO*\t*WORLD*";
      var expected = "hello world";
      assertEquals(expected, convertOneLine(transformer, input));
    }

    @Test
    @DisplayName("Create one transformer with a chain of l")
    public void createOneTransformerChain() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("lllll"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      // Should still work (multiple lowercase applications)
      assertEquals("hello", convertOneLine(transformer, "HELLO"));
      assertEquals("world", convertOneLine(transformer, "world")); // already lowercase
    }

    @Test
    @DisplayName("Create one transformer with null input")
    public void createOneTransformerWithNullInput() {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("l"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      assertThrows(NullPointerException.class, () -> convertOneLine(transformer, null));
    }

    @Test
    @DisplayName("Create one transformer is reusable")
    public void createOneTransformerReusable() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("l*2"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      assertEquals("**hello**", convertOneLine(transformer, "*HELLO*"));
      assertEquals("**world**", convertOneLine(transformer, "*WORLD*"));
      assertEquals("test", convertOneLine(transformer, "TEST"));
    }

    @Test
    @DisplayName("Create one transformer does not modify original list")
    public void createOneTransformerDoesNotModifyOriginalList() throws IOException {
      var transformersList = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("l"));
      var originalSize = transformersList.size();

      var transformer = StreamEditor.createOneTransformer(transformersList);

      assertEquals(originalSize, transformersList.size()); // List should be unchanged
      assertEquals("hello", convertOneLine(transformer, "HELLO"));
    }

    @Test
    @DisplayName("Create one transformer with some star variations")
    public void createOneTransformerSomeStarVariations() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("*0*1*2"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      assertEquals("hello", convertOneLine(transformer, "*hello*"));
      assertEquals("", convertOneLine(transformer, "*"));
    }

    @Test
    @DisplayName("Create one transformer handling empty string")
    public void createOneTransformerEmptyStringHandling() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(StreamEditor.characterIterator("lr*5"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      assertEquals("", convertOneLine(transformer, ""));
    }

    @Test
    @DisplayName("Create one transformer acts as identity when empty")
    public void createOneTransformerIdentityWhenEmpty() throws IOException {
      var transformer = StreamEditor.createOneTransformer(List.of());

      // Should act as identity function
      var testInputs = List.of("hello", "WORLD", "test\ttabs", "*stars*", "", "123");
      for (var input : testInputs) {
        assertEquals(input, convertOneLine(transformer, input));
      }
    }

    @Test
    @DisplayName("Rewrite one line with lowercase then lowercase again")
    public void rewriteOneLineLowerThenLowerCaseAgain() throws IOException {
      var transformer = StreamEditor.parseTransformer("ll");
      var stringReader = new StringReader("hEllo\n");
      var writer = new CharArrayWriter();
      try(var reader = new BufferedReader(stringReader)) {
        StreamEditor.rewrite(reader, writer, transformer);
      }
      assertEquals("hello\n", writer.toString());
    }

    @Test
    @DisplayName("Rewrite one line with two stars then lowercase")
    public void rewriteOneLineStarTwoThenLowerCase() throws IOException {
      var transformer = StreamEditor.parseTransformer("*2l");
      var stringReader =
          new StringReader("*fOO**\n");
      var writer = new CharArrayWriter();
      try(var reader = new BufferedReader(stringReader)) {
        StreamEditor.rewrite(reader, writer, transformer);
      }
      assertEquals("**foo****\n", writer.toString());
    }

    @Test
    @DisplayName("Rewrite one line with lowercase then nine stars")
    public void rewriteOneLineLowerCaseThenStarNine() throws IOException {
      var transformer = StreamEditor.parseTransformer("l*9");
      var stringReader = new StringReader("*fOO**\n");
      var writer = new CharArrayWriter();
      try(var reader = new BufferedReader(stringReader)) {
        StreamEditor.rewrite(reader, writer, transformer);
      }
      assertEquals("*********foo******************\n", writer.toString());
    }

    @Test
    @DisplayName("Rewrite one line with no command")
    public void rewriteOneLineNoCommand() throws IOException {
      var transformer = StreamEditor.parseTransformer("");
      var stringReader = new StringReader("HeLLo\n");
      var writer = new CharArrayWriter();
      try(var reader = new BufferedReader(stringReader)) {
        StreamEditor.rewrite(reader, writer, transformer);
      }
      assertEquals("HeLLo\n", writer.toString());
    }

    @Test
    @DisplayName("Create one transformer performance with many transformers")
    public void createOneTransformerPerformanceWithManyTransformers() throws IOException {
      var commandString = "l".repeat(100);
      var transformers = StreamEditor.parseAllTransformers(
          StreamEditor.characterIterator(commandString));
      var transformer = StreamEditor.createOneTransformer(transformers);

      assertEquals(100, transformers.size());
      assertEquals("hello world", convertOneLine(transformer, "HELLO WORLD"));
    }

    @Test
    @DisplayName("Create one transformer, complete workflow")
    public void createOneTransformerCompleteWorkflow() throws IOException {
      var transformers = StreamEditor.parseAllTransformers(
          StreamEditor.characterIterator("*3lr*0"));
      var transformer = StreamEditor.createOneTransformer(transformers);

      var input = "*HELLO*\t*WORLD*";
      // *3: "*HELLO*\t*WORLD*" -> "***HELLO***\t***WORLD***"
      // l:  "***HELLO***\t***WORLD***" -> "***hello***\t***world***"
      // r:  "***hello***\t***world***" -> "***hello*** ***world***"
      // *0: "***hello*** ***world***" -> "hello world"
      assertEquals("hello world", convertOneLine(transformer, input));
    }
  }


  @Nested
  public class Q10 {

    private static String rewriteUsingFiles(String text, String commands) throws IOException {
      var directory = Files.createTempDirectory("stream-editor-test");
      try {
        var inputPath = directory.resolve("input.txt");
        var outputPath = directory.resolve("output.txt");
        try {
          Files.writeString(inputPath, text);
          StreamEditor.rewrite(inputPath, outputPath, StreamEditor.parseTransformer(commands));
          return Files.readString(outputPath);
        } finally {
          Files.deleteIfExists(outputPath);
          Files.deleteIfExists(inputPath);
        }
      } finally {
        Files.delete(directory);
      }
    }

    @Test
    @DisplayName("Rewrite using files one line with lowercase")
    public void rewriteUsingFilesOneLineLowerCase() throws IOException {
      var text = """
          hello my name is Joey
          """;
      var result = rewriteUsingFiles(text, "l");
      assertEquals("""
        hello my name is joey
        """, result);
    }

    @Test
    @DisplayName("Rewrite using files a lot of lines with lowercase")
    public void rewriteUsingFilesALotOfLineslowerCase() throws IOException {
      var text = IntStream.range(0, 100_000).mapToObj(i -> i + "\n").collect(joining(""));
      var result = rewriteUsingFiles(text, "l");
      assertEquals(text, result);
    }

    @Test
    @DisplayName("Rewrite using files one line with replace")
    public void rewriteUsingFilesOneLineReplace() throws IOException {
      var text = """
          hello\tmy\tname\tis\tJoey
          """;
      var result = rewriteUsingFiles(text, "r");
      assertEquals("""
        hello my name is Joey
        """, result);
    }

    @Test
    @DisplayName("Rewrite using files a lot of lines with replace")
    public void rewriteUsingFilesALotOfLinesReplace() throws IOException {
      var text = IntStream.range(0, 100_000).mapToObj(i -> i + "\n").collect(joining(""));
      var result = rewriteUsingFiles(text, "r");
      assertEquals(text, result);
    }

    @Test
    public void rewriteUsingFilesOneLineStarTwo() throws IOException {
      var text = """
          hello my *name* is Joey
          """;
      var result = rewriteUsingFiles(text, "*2");
      assertEquals("""
        hello my **name** is Joey
        """, result);
    }

    @Test
    @DisplayName("Rewrite using files a lot of lines with two stars")
    public void rewriteUsingFilesALotOfLinesStarTwo() throws IOException {
      var text = IntStream.range(0, 100_000).mapToObj(i -> i + "\n").collect(joining(""));
      var result = rewriteUsingFiles(text, "*2");
      assertEquals(text, result);
    }

    @Test
    @DisplayName("Rewrite using files one line with two stars then five stars")
    public void rewriteUsingFilesOneLineStarTwoStarFive() throws IOException {
      var text = """
          hello my *name* is Joey
          """;
      var result = rewriteUsingFiles(text, "*2*5");
      assertEquals("""
        hello my **********name********** is Joey
        """, result);
    }

    @Test
    @DisplayName("Rewrite using files a lot of lines with two stars then five stars")
    public void rewriteUsingFilesALotOfLinesStarTwoStarFive() throws IOException {
      var text = IntStream.range(0, 100_000).mapToObj(i -> i + "\n").collect(joining(""));
      var result = rewriteUsingFiles(text, "*2*5");
      assertEquals(text, result);
    }

    @Test
    @DisplayName("Rewrite using files, arguments can not be null")
    public void rewriteUsingFilesPreconditions() {
      var transformer = StreamEditor.parseTransformer("l");
      assertAll(
          () -> assertThrows(NullPointerException.class, () -> StreamEditor.rewrite(null, Path.of("."), transformer)),
          () -> assertThrows(NullPointerException.class, () -> StreamEditor.rewrite(Path.of("."), null, transformer)),
          () -> assertThrows(NullPointerException.class, () -> StreamEditor.rewrite(Path.of("."), Path.of("."), null))
      );
    }
  }
}