package io.termd.core.term;

import io.termd.core.util.Helper;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static org.junit.Assert.*;

/**
 * @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
 */
public class TermInfoTest {

  /**
   * Compare two terminal escape sequences.
   *
   * @param expected the expected sequence
   * @param actual the sequence
   */
  public static void assertSequenceEquals(String expected, String actual) {
    try {
      assertEquals(expected, actual);
    } catch (AssertionError e) {
      throw new AssertionError("Was expecting sequence <" + Helper.escape(actual) + "> to be equals to <"
          + Helper.escape(expected) + ">");
    }
  }

  @Test
  public void testParseComment() throws Exception {
    TermInfoParser parser = new TermInfoParser("#\n");
    parser.parseCommentLine();
  }

  @Test
  public void testParseHeaderLine() throws Exception {
    assertParseHeaderLine("foo,\n", "foo");
    assertParseHeaderLine("foo bar,\n", "foo bar");
    assertParseHeaderLine("foo,bar,\n", "foo,bar");
    assertParseHeaderLine("foo|bar,\n", "foo", "bar");
    assertParseHeaderLine("foo|bar juu,\n", "foo", "bar juu");
    assertParseHeaderLine("foo|bar,juu,\n", "foo", "bar,juu");
    assertParseHeaderLine("foo|bar|juu,\n", "foo", "bar", "juu");
    failParseHeaderLine("");
    failParseHeaderLine("foo,");
    failParseHeaderLine("foo|");
    failParseHeaderLine("foo|,");
  }

  private void assertParseHeaderLine(String s, String... expected) {
    try {
      TermInfoParser parser = new TermInfoParser(s);
      List<String> actual = parser.parseHeaderLine();
      assertEquals(Arrays.asList(expected), actual);
    } catch (ParseException e) {
      throw new AssertionError(e);
    }
  }

  private void failParseHeaderLine(String s) {
    try {
      TermInfoParser parser = new TermInfoParser(s);
      parser.parseHeaderLine();
      fail();
    } catch (ParseException ignore) {
    } catch (TokenMgrError ignore) {
    }
  }

  @Test
  public void testParseFeatureLine() throws Exception {
    assertFeatureLine(" foo=bar\\\\,\n", Feature.create("foo", new Sequence("bar\\")));
    assertFeatureLine(" foo,\n", Feature.create("foo", true));
    assertFeatureLine(" foo@,\n", Feature.create("foo", false));
    assertFeatureLine(" foo,bar,\n", Feature.create("foo", true), Feature.create("bar", true));
    assertFeatureLine(" foo, bar,\n", Feature.create("foo", true), Feature.create("bar", true));
    assertFeatureLine(" foo=,\n", Feature.create("foo", new Sequence(Collections.<OpCode>emptyList())));
    assertFeatureLine(" foo=\\s,\n", Feature.create("foo", new Sequence(" ")));
    assertFeatureLine(" foo=\\\\,\n", Feature.create("foo", new Sequence("\\")));
    assertFeatureLine(" foo=\\,,\n", Feature.create("foo", new Sequence(",")));
    assertFeatureLine(" foo=\\^,\n", Feature.create("foo", new Sequence("^")));
    assertFeatureLine(" foo=^\\,\n", Feature.create("foo", new Sequence(Character.toString((char) 28))));
    assertFeatureLine(" foo=^^,\n", Feature.create("foo", new Sequence(Character.toString((char) 30))));
    assertFeatureLine(" foo=bar,\n", Feature.create("foo", new Sequence("bar")));
    assertFeatureLine(" foo=bar,juu=daa,\n", Feature.create("foo", new Sequence("bar")), Feature.create("juu", new Sequence("daa")));
    assertFeatureLine(" foo=bar, juu=daa,\n", Feature.create("foo", new Sequence("bar")), Feature.create("juu", new Sequence("daa")));
    assertFeatureLine(" foo,bar=juu,\n", Feature.create("foo", true), Feature.create("bar", new Sequence("juu")));
    assertFeatureLine(" foo=bar,juu,\n", Feature.create("foo", new Sequence("bar")), Feature.create("juu", true));
    assertFeatureLine(" foo=b\\,ar,\n", Feature.create("foo", new Sequence("b,ar")));
    assertFeatureLine(" foo#1234,\n", Feature.create("foo", 1234));
    assertFeatureLine(" foo#0,\n", Feature.create("foo", 0));
    assertFeatureLine(" foo#0x1234,\n", Feature.create("foo", 0x1234));
    assertFeatureLine(" foo#01234,\n", Feature.create("foo", 01234));
    failParseFeatureLine(" ");
    failParseFeatureLine(" foo,");
    failParseFeatureLine(" foo#,\n");
    failParseFeatureLine(" foo#a,\n");
  }

  @Test
  public void testStringParameterized() {
    String[] tests = {
        // Parameters
        "%p1",
        "%p2",
        "%p3",
        "%p4",
        "%p5",
        "%p6",
        "%p7",
        "%p7",
        "%p8",
        "%p9",
        // Printf
        "%d",
        "%o",
        "%x",
        "%X",
        "%s",
        "%+d",
        "%#d",
        "% d",
        //
//        "%{1}",
    };
/*
    for (int i = 0;i < tests.length;i += 1) {
      String info = " " + Capability.acs_chars.name + "=" + tests[i] + ",\n";
      assertParseFeatureLine(info);
    }
*/
  }

  @Test
  public void testOpParam() {
    assertParseInstr("%p1", new OpCode.PushParam(1));
    assertParseInstr("%p2", new OpCode.PushParam(2));
    assertParseInstr("%p3", new OpCode.PushParam(3));
    assertParseInstr("%p4", new OpCode.PushParam(4));
    assertParseInstr("%p5", new OpCode.PushParam(5));
    assertParseInstr("%p6", new OpCode.PushParam(6));
    assertParseInstr("%p7", new OpCode.PushParam(7));
    assertParseInstr("%p8", new OpCode.PushParam(8));
    assertParseInstr("%p9", new OpCode.PushParam(9));
  }

  @Test
  public void testOpIntegerConstant() {
    assertParseInstr("%{0}", new OpCode.PushConstant(0, true));
    assertParseInstr("%{1}", new OpCode.PushConstant(1, true));
    assertParseInstr("%{01}", new OpCode.PushConstant(1, true));
    assertParseInstr("%{2}", new OpCode.PushConstant(2, true));
    assertParseInstr("%{10}", new OpCode.PushConstant(10, true));
    assertParseInstr("%{11}", new OpCode.PushConstant(11, true));
  }

  @Test
  public void testOpPrintf() throws ParseException {

    // Specifier
    assertParseOpPrintf("%d", null, null, null, 'd');
    assertParseOpPrintf("%o", null, null, null, 'o');
    assertParseOpPrintf("%x", null, null, null, 'x');
    assertParseOpPrintf("%X", null, null, null, 'X');

    // Flags
    assertParseOpPrintf("%#", '#', null, null, null);
    assertParseOpPrintf("% ", ' ', null, null, null);
    assertParseOpPrintf("%:#", '#', null, null, null);
    assertParseOpPrintf("%:-", '-', null, null, null);
    assertParseOpPrintf("%:+", '+', null, null, null);
    assertParseOpPrintf("%: ", ' ', null, null, null);

    // Width
    assertParseOpPrintf("%0", null, "0", null, null);
    assertParseOpPrintf("%10", null, "10", null, null);

    // Precision
    assertParseOpPrintf("%.0", null, null, "0", null);
    assertParseOpPrintf("%.10", null, null, "10", null);

    // Various
    assertParseOpPrintf("% 10", ' ', "10", null, null);
    assertParseOpPrintf("%:-16s", '-', "16", null, 's');
    assertParseOpPrintf("%:-16.16s", '-', "16", "16", 's');
    assertParseOpPrintf("%03d", null, "03", null, 'd');
  }

  @Test
  public void testOpVariable() {
    assertParseInstr("%Pa", new OpCode.SetPopVar('a'));
    assertParseInstr("%Pz", new OpCode.SetPopVar('z'));
    assertParseInstr("%PA", new OpCode.SetPopVar('A'));
    assertParseInstr("%PZ", new OpCode.SetPopVar('Z'));
    assertParseInstr("%ga", new OpCode.GetPushVar('a'));
    assertParseInstr("%gz", new OpCode.GetPushVar('z'));
    assertParseInstr("%gA", new OpCode.GetPushVar('A'));
    assertParseInstr("%gZ", new OpCode.GetPushVar('Z'));
  }

  @Test
  public void testOpBit() {
    assertParseInstr("%&", OpCode.Bit.AND);
    assertParseInstr("%|", OpCode.Bit.OR);
    assertParseInstr("%^", OpCode.Bit.XOR);
    assertParseInstr("%~", OpCode.Bit.NEG);
  }

  @Test
  public void testOpLogical() {
    assertParseInstr("%=", OpCode.Logical.EQ);
    assertParseInstr("%>", OpCode.Logical.GT);
    assertParseInstr("%<", OpCode.Logical.LT);
    assertParseInstr("%A", OpCode.Logical.AND);
    assertParseInstr("%O", OpCode.Logical.OR);
    assertParseInstr("%!", OpCode.Logical.NEG);
  }

  @Test
  public void testOpArithmetic() {
    assertParseInstr("%+", OpCode.Arithmetic.PLUS);
    assertParseInstr("%-", OpCode.Arithmetic.MINUS);
    assertParseInstr("%*", OpCode.Arithmetic.MUL);
    assertParseInstr("%/", OpCode.Arithmetic.DIV);
    assertParseInstr("%m", OpCode.Arithmetic.MOD);
  }

  @Test
  public void testOpAdd1ToParams() {
    assertParseInstr("%i", OpCode.Add1ToParams.INSTANCE);
  }

  @Test
  public void testIfThenElse() {
    List<OpCode> p1 = Arrays.<OpCode>asList(new OpCode.PushParam(1));
    List<OpCode> p2 = Arrays.<OpCode>asList(new OpCode.PushParam(2));
    List<OpCode> p3 = Arrays.<OpCode>asList(new OpCode.PushParam(3));
    List<OpCode> p4 = Arrays.<OpCode>asList(new OpCode.PushParam(4));
    List<OpCode> p5 = Arrays.<OpCode>asList(new OpCode.PushParam(5));
    List<OpCode> p6 = Arrays.<OpCode>asList(new OpCode.PushParam(6));
    List<OpCode> p7 = Arrays.<OpCode>asList(new OpCode.PushParam(7));
    assertParseInstr("%?%p1%t%p2%;", new OpCode.If(p1, new OpCode.Then(p2)));
    assertParseInstr("%?%p1%t%p2%e%p3%;", new OpCode.If(p1, new OpCode.Then(p2, new OpCode.Else(p3))));
    assertParseInstr("%?%p1%t%p2%e%p3%t%p4%;", new OpCode.If(p1, new OpCode.Then(p2, new OpCode.If(p3, new OpCode.Then(p4)))));
    assertParseInstr("%?%p1%t%p2%e%p3%t%p4%e%p5%;", new OpCode.If(p1, new OpCode.Then(p2, new OpCode.If(p3, new OpCode.Then(p4, new OpCode.Else(p5))))));
    assertParseInstr("%?%p1%t%p2%e%p3%t%p4%e%p5%t%p6%;", new OpCode.If(p1, new OpCode.Then(p2, new OpCode.If(p3, new OpCode.Then(p4, new OpCode.If(p5, new OpCode.Then(p6)))))));
    assertParseInstr("%?%p1%t%p2%e%p3%t%p4%e%p5%t%p6%e%p7%;", new OpCode.If(p1, new OpCode.Then(p2, new OpCode.If(p3, new OpCode.Then(p4, new OpCode.If(p5, new OpCode.Then(p6, new OpCode.Else(p7))))))));
    List<OpCode> p1p2 = Arrays.<OpCode>asList(new OpCode.PushParam(1), new OpCode.PushParam(2));
    List<OpCode> p3p4 = Arrays.<OpCode>asList(new OpCode.PushParam(3), new OpCode.PushParam(4));
    List<OpCode> p5p6 = Arrays.<OpCode>asList(new OpCode.PushParam(5), new OpCode.PushParam(6));
    assertParseInstr("%?%p1%p2%t%p3%p4%e%p5%p6%;", new OpCode.If(p1p2, new OpCode.Then(p3p4, new OpCode.Else(p5p6))));
  }

  @Test
  public void testOpEsc() {
    assertParseInstr("%%", OpCode.Percent.INSTANCE);
  }

  @Test
  public void testOpStrLen() {
    assertParseInstr("%l", OpCode.PushStrLen.INSTANCE);
  }

  @Test
  public void testOpPushConstant() {
    assertParseInstr("%'a'", new OpCode.PushConstant('a', false));
    assertParseInstr("%'\''", new OpCode.PushConstant('\'', false));
    assertParseInstr("%'\\123'", new OpCode.PushConstant((char) 83, false));
    assertParseInstr("%'\\0'", new OpCode.PushConstant(0, false));
  }

  @Test
  public void testOpPrintPop() {
    assertParseInstr("%c", OpCode.PrintChar.INSTANCE);
    assertParseInstr("%s", new OpCode.Printf(null, null, null, 's'));
  }

  private void assertParseOpPrintf(String s, Character flag, String width, String precision, Character specifier) {
    assertParseInstr(s, new OpCode.Printf(flag, width, precision, specifier));
  }

  private void assertParseInstr(String s, OpCode expected) {
    try {
      OpCode op = new TermInfoParser(s).parseSingleOpCode();
      assertEquals(expected, op);
    } catch (ParseException e) {
      throw new AssertionError(e);
    }
  }

  @Test
  public void testStringSpecialCharsFeature() {
    String[] tests = {
        "\\a", String.valueOf((char) 7),
        "\\A", String.valueOf((char) 1),
        "\\b", String.valueOf((char) 8),
        "\\e", String.valueOf((char) 27),
        "\\E", String.valueOf((char) 27),
        "\\f", String.valueOf((char) 12),
        "\\n", String.valueOf('\n'),
        "\\r", String.valueOf('\r'),
        "\\s", String.valueOf(' '),
        "\\t", String.valueOf('\t'),
        "\\^", String.valueOf('^'),
        "\\\\", String.valueOf('\\'),
        "\\:", String.valueOf(':'),
        "\\0", String.valueOf((char) 0),
        "\\030", String.valueOf((char) 24),
        "\\101", String.valueOf((char) 65),
        "\\01ab", String.valueOf((char) 0) + "1ab",
        "^c", String.valueOf((char)('C' - 64)),
    };
    for (int i = 0;i < tests.length;i += 2) {
      String info = " " + Capability.acs_chars.name + "=" + tests[i] + ",\n";
      assertFeatureLine(info, Feature.create(Capability.acs_chars.name, new Sequence(tests[i + 1])));
    }
  }

  private void assertFeatureLine(String s, Feature<?>... expected) {
    List<Feature<?>> features = assertParseFeatureLine(s);
    assertEquals(Arrays.asList(expected), features);
  }

  private List<Feature<?>> assertParseFeatureLine(String s) {
    try {
      final List<Feature<?>> features = new ArrayList<>();
      TermInfoParser parser = new TermInfoParser(s);
      ParserHandler handler = new ParserHandler() {
        @Override
        public void addBooleanFeature(String name, boolean value) {
          features.add(Feature.create(name, value));
        }
        @Override
        public void addStringFeature(String name, Sequence value) {
          features.add(Feature.create(name, value));
        }
        @Override
        public void addNumericFeature(String name, int value) {
          features.add(Feature.create(name, value));
        }
      };
      parser.parseFeatureLine(handler);
      return features;
    } catch (ParseException e) {
      throw new AssertionError(e);
    }
  }

  private void failParseFeatureLine(String s) {
    try {
      TermInfoParser parser = new TermInfoParser(s);
      parser.parseFeatureLine();
      fail();
    } catch (ParseException ignore) {
    } catch (TokenMgrError ignore) {
    }
  }

  @Test
  public void testParseDevice() {
    assertParseDevice("abc|def,\n foo,bar,\n");
  }

  private void assertParseDevice(String s) {
    try {
      TermInfoParser parser = new TermInfoParser(s);
      parser.parseDevice();
    } catch (ParseException e) {
      throw new AssertionError(e);
    }
  }

  @Test
  public void testParseDatabase() {
    assertParseDatabase("" +
            "# hello \n" +
            "abc|def,\n" +
            " foo,bar,\n" +
            "juu|daa,\n" +
            " bilto|bilta,\n"
    );
  }

  private void assertParseDatabase(String s) {
    try {
      TermInfoParser parser = new TermInfoParser(s);
      parser.parseDatabase();
    } catch (ParseException e) {
      throw new AssertionError(e);
    }
  }

  @Test
  public void testUse() {
    TermInfo info = assertBuildDevices(
        "a,\n" +
            " bw,\n" +
            "b,\n" +
            " use=a,\n" +
            "c,\n" +
            " use=a,bw@,\n"
    );
    assertTrue(info.getDevice("a").getFeature(Capability.auto_left_margin));
    assertTrue(info.getDevice("b").getFeature(Capability.auto_left_margin));
    assertFalse(info.getDevice("c").getFeature(Capability.auto_left_margin));
  }

  @Test
  public void testUseCycle() throws Exception {
    TermInfoBuilder builder = new TermInfoBuilder();
    new TermInfoParser("" +
        "a,\n" +
        " use=b,\n" +
        "b,\n" +
        " use=c,\n" +
        "c,\n" +
        " use=a,\n"
    ).parseDatabase(builder);
    try {
      builder.build();
      fail();
    } catch (IllegalStateException ignore) {
    }
  }

  @Test
  public void testUseNotFound() throws Exception {
    TermInfoBuilder builder = new TermInfoBuilder();
    new TermInfoParser("" +
        "a,\n" +
        " use=b,\n"
    ).parseDatabase(builder);
    try {
      builder.build();
      fail();
    } catch (IllegalStateException ignore) {
    }
  }

  private TermInfo assertBuildDevices(String s) {
    try {
      TermInfoParser parser = new TermInfoParser(s);
      TermInfoBuilder builder = new TermInfoBuilder();
      parser.parseDatabase(builder);
      return builder.build();
    } catch (ParseException e) {
      throw new AssertionError(e);
    }
  }

  @Test
  public void testParse() throws Exception {
    ByteArrayOutputStream res = new ByteArrayOutputStream();
    try (InputStream in = TermInfoParser.class.getResourceAsStream("terminfo.src")) {
      byte[] buffer = new byte[256];
      while (true) {
        int len = in.read(buffer);
        if (len == -1) {
          break;
        }
        res.write(buffer, 0, len);
      }
    }
    String s = res.toString("ISO-8859-1");
    TermInfoParser parser = new TermInfoParser(s);
    TermInfoBuilder builder = new TermInfoBuilder();
    parser.parseDatabase(builder);
    TermInfo info = builder.build();
    Collection<Device> entries = info.devices();
    assertEquals(2645, entries.size());
    Device device = info.getDevice("xterm-color");
    assertNotNull(device);
  }

  @Test
  public void testSomeEval() throws Exception {
    Sequence cup = TermInfo.defaultInfo().getDevice("xterm-color").getFeature(Capability.cursor_address);
    Sequence smcup = TermInfo.defaultInfo().getDevice("xterm-color").getFeature(Capability.enter_ca_mode);
    assertSequenceEquals("\033[1;1H", cup.eval("0", "0"));
    assertSequenceEquals("\0337\033[?47h", smcup.eval());
    Sequence setb = TermInfo.defaultInfo().getDevice("xtermc").getFeature(Capability.set_background);
    assertSequenceEquals("\033[44m", setb.eval("1"));
    assertSequenceEquals("\033[42m", setb.eval("2"));
    assertSequenceEquals("\033[46m", setb.eval("3"));
    assertSequenceEquals("\033[41m", setb.eval("4"));
    assertSequenceEquals("\033[45m", setb.eval("5"));
    assertSequenceEquals("\033[43m", setb.eval("6"));
    assertSequenceEquals("\033[47m", setb.eval("7"));
    assertSequenceEquals("\033[412m", setb.eval("12"));

    Sequence pfkey = TermInfo.defaultInfo().getDevice("tvi924").getFeature(Capability.pkey_key);
    assertEquals("\033|2foo\031", pfkey.eval("1", "foo"));
  }

  @Test
  public void testEvalPushParam() throws Exception {
    for (int i = 0;i < 9;i++) {
      OpCode.PushParam opcode = new OpCode.PushParam(1 + i);
      String[] parameters = new String[1 + i];
      parameters[i] = "10";
      StringBuilder result = new StringBuilder();
      EvalContext context = new EvalContext(parameters, result);
      opcode.eval(context);
      assertEquals(Arrays.asList("10"), context.stack);
      assertEquals("", result.toString());
    }
  }

  @Test
  public void testEvalPushParamInvalidIndex() throws Exception {
    for (int index : Arrays.asList(0, 10)) {
      try {
        new OpCode.PushParam(index);
        fail();
      } catch (IllegalArgumentException ignore) {
      }
    }
  }

  @Test
  public void testEvalPushParamNoParam() throws Exception {
    StringBuilder result = new StringBuilder();
    EvalContext context = new EvalContext(new String[0], result);
    OpCode.PushParam opcode = new OpCode.PushParam(1);
    try {
      opcode.eval(context);
      fail();
    } catch (IllegalArgumentException ignore) {
      assertEquals(Arrays.<String>asList(), context.stack);
      assertEquals("", result.toString());
    }
  }

  @Test
  public void testEvalPushConstant() throws Exception {
    OpCode.PushConstant opcode = new OpCode.PushConstant(65, false);
    StringBuilder result = new StringBuilder();
    EvalContext context = new EvalContext(new String[0], result);
    opcode.eval(context);
    assertEquals(Arrays.asList("A"), context.stack);
    assertEquals("", result.toString());
  }

  @Test
  public void testEvalLiteral() throws Exception {
    for (String test : Arrays.asList("a", "\r\n", "\033")) {
      OpCode.Literal opcode = new OpCode.Literal(test);
      StringBuilder result = new StringBuilder();
      EvalContext context = new EvalContext(new String[0], result);
      opcode.eval(context);
      assertEquals(Arrays.<String>asList(), context.stack);
      assertEquals(test, result.toString());
    }
  }

  @Test
  public void testEvalArithmeticPlus() {
    EvalContext context = new EvalContext(new String[0], cp -> {}).push("0").push("0");
    OpCode.Arithmetic.PLUS.eval(context);
    assertEquals("0", context.pop());
    context = new EvalContext(new String[0], cp -> {}).push("0").push("10");
    OpCode.Arithmetic.PLUS.eval(context);
    assertEquals("10", context.pop());
    context = new EvalContext(new String[0], cp -> {}).push("10").push("0");
    OpCode.Arithmetic.PLUS.eval(context);
    assertEquals("10", context.pop());
    context = new EvalContext(new String[0], cp -> {}).push("10").push("5");
    OpCode.Arithmetic.PLUS.eval(context);
    assertEquals("15", context.pop());
  }

  @Test
  public void testEvalLogicalEq() {
    EvalContext context = new EvalContext(new String[0], cp -> {}).push("0").push("0");
    OpCode.Logical.EQ.eval(context);
    assertEquals("1", context.pop());
    context = new EvalContext(new String[0], cp -> {}).push("0").push("1");
    OpCode.Logical.EQ.eval(context);
    assertEquals("0", context.pop());
    context = new EvalContext(new String[0], cp -> {}).push("1").push("1");
    OpCode.Logical.EQ.eval(context);
    assertEquals("1", context.pop());
    context = new EvalContext(new String[0], cp -> {}).push("150").push("150");
    OpCode.Logical.EQ.eval(context);
    assertEquals("1", context.pop());
    context = new EvalContext(new String[0], cp -> {}).push("150").push("4");
    OpCode.Logical.EQ.eval(context);
    assertEquals("0", context.pop());
  }

  @Test
  public void testEvalIfThenElse() throws Exception {
    String[][] tests = {
        { "%?%{0}%tA%;", "" },
        { "%?%{1}%tA%;", "A" },
        { "%?%{0}%tA%eB%;", "B" },
        { "%?%{1}%tA%eB%;", "A" },
        { "%?%{0}%tA%e%{1}%tB%;", "B" },
        { "%?%{1}%tA%e%{1}%tB%;", "A" },
        { "%?%{0}%tA%e%{0}%tB%;", "" },
        { "%?%{1}%tA%e%{0}%tB%;", "A" },
        { "%?%{0}%tA%e%{1}%tB%eC%;", "B" },
        { "%?%{1}%tA%e%{1}%tB%eC%;", "A" },
        { "%?%{0}%tA%e%{0}%tB%eC%;", "C" },
        { "%?%{1}%tA%e%{0}%tB%eC%;", "A" },
    };
    for (String[] test : tests) {
      StringBuilder result = new StringBuilder();
      OpCode opCode = new TermInfoParser(test[0]).parseSingleOpCode();
      opCode.eval(new EvalContext(new String[0], result)) ;
      assertEquals(test[1], result.toString());
    }
  }

  @Test
  public void testEvalPrintChar() throws Exception {
    StringBuilder result = new StringBuilder();
    EvalContext context = new EvalContext(new String[0], result).push("65");
    OpCode.PrintChar.INSTANCE.eval(context);
    assertEquals("A", result.toString());
  }

  @Test
  public void testEvalPrintf() throws Exception {
    StringBuilder result = new StringBuilder();
    EvalContext context = new EvalContext(new String[0], result).push("hello");
    new OpCode.Printf(null, null, null, 's').eval(context);
    assertEquals("hello", result.toString());
  }
}
