// Copyright (C) 2006 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.caja.parser;

import com.google.caja.lexer.FilePosition;
import com.google.caja.lexer.JsTokenType;
import com.google.caja.lexer.Token;
import com.google.caja.lexer.TokenConsumer;
import com.google.caja.parser.js.AbstractExpression;
import com.google.caja.parser.js.Block;
import com.google.caja.parser.js.ExpressionStmt;
import com.google.caja.parser.js.IntegerLiteral;
import com.google.caja.parser.js.LabeledStatement;
import com.google.caja.parser.js.LabeledStmtWrapper;
import com.google.caja.parser.js.Noop;
import com.google.caja.parser.js.StringLiteral;
import com.google.caja.render.JsPrettyPrinter;
import com.google.caja.reporting.MessageContext;
import com.google.caja.reporting.RenderContext;
import com.google.caja.util.CajaTestCase;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author [email protected] (Mike Samuel)
 */
@SuppressWarnings("static-method")
public class ParseTreeNodeTest extends CajaTestCase {
  LabeledStmtWrapper root;
  LabeledStmtWrapper b9;

  @Override
  protected void setUp() throws Exception {
    super.setUp();

    FilePosition unk = FilePosition.UNKNOWN;

    // $0: {
    //   $1: {
    //     2;
    //     3;
    //     4;
    //   }
    //   $5: {
    //     6;
    //     7;
    //     8;
    //   }
    // }
    // $9: {
    //   10;
    //   11;
    //   12;
    // }

    ExpressionStmt[] b = new ExpressionStmt[13];
    for (int i = b.length; --i >= 0;) {
      b[i] = new ExpressionStmt(unk, new IntegerLiteral(unk, i));
    }

    LabeledStmtWrapper b1 = new LabeledStmtWrapper(
        unk, "$1", new Block(unk, Arrays.asList(b[2], b[3], b[4])));

    LabeledStmtWrapper b5 = new LabeledStmtWrapper(
        unk, "$5", new Block(unk, Arrays.asList(b[6], b[7], b[8])));
    root = new LabeledStmtWrapper(
        unk, "$0", new Block(unk, Arrays.asList(b1, b5)));

    b9 = new LabeledStmtWrapper(
        unk, "$9", new Block(unk, Arrays.asList(b[10], b[11], b[12])));
  }

  @Override
  protected void tearDown() throws Exception {
    super.tearDown();
    root = null;
  }

  public final void testFormatTree() throws Exception {
    StringBuilder sb = new StringBuilder();
    MessageContext mc = new MessageContext();
    root.formatTree(mc, 0, sb);
    assertEquals(
        "LabeledStmtWrapper : $0\n"
        + "  Block\n"
        + "    LabeledStmtWrapper : $1\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 2\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 3\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 4\n"
        + "    LabeledStmtWrapper : $5\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 6\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 7\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 8",
        sb.toString());
  }

  public final void testRender() {
    StringBuilder sb = new StringBuilder();
    TokenConsumer tc = new JsPrettyPrinter(sb);
    root.render(new RenderContext(tc));
    tc.noMoreTokens();
    assertEquals(
        "$0: {\n"
        + "  $1: {\n"
        + "    2;\n"
        + "    3;\n"
        + "    4;\n"
        + "  }\n"
        + "  $5: {\n"
        + "    6;\n"
        + "    7;\n"
        + "    8;\n"
        + "  }\n"
        + "}",
        sb.toString());
  }

  public final void testAcceptPreOrder() {
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5, 6, 7, 8]", ie.getNums().toString());
  }

  public final void testAcceptPreOrderReturnHandling() {
    IntEnqueuerExcept ie = new IntEnqueuerExcept(6);
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5, 6, 7, 8]", ie.getNums().toString());

    ie = new IntEnqueuerExcept(1);
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 5, 6, 7, 8]", ie.getNums().toString());
  }

  public final void testAcceptPostOrder() {
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPostOrder(ie, null);
    assertEquals("[2, 3, 4, 1, 6, 7, 8, 5, 0]", ie.getNums().toString());
  }

  public final void testAcceptPostOrderReturnHandling() {
    IntEnqueuerExcept ie = new IntEnqueuerExcept(6);
    root.acceptPostOrder(ie, null);
    assertEquals("[2, 3, 4, 1, 6]", ie.getNums().toString());
  }

  public final void testVisitPreOrder() {
    final List<Number> nums = Lists.newArrayList();
    root.visitPreOrder(new ParseTreeNodeVisitor() {
      public boolean visit(ParseTreeNode node) {
        if (node instanceof IntegerLiteral) {
          nums.add(((IntegerLiteral)node).getValue());
        }
        return true;
      }
    });
    assertEquals("[2, 3, 4, 6, 7, 8]", nums.toString());
  }

  public final void testVisitPreOrderSkip() {
    final List<Number> nums = Lists.newArrayList();
    root.visitPreOrder(new ParseTreeNodeVisitor() {
      public boolean visit(ParseTreeNode node) {
        if (node instanceof IntegerLiteral) {
          nums.add(((IntegerLiteral)node).getValue());
        } else if (node instanceof LabeledStatement) {
          String label = ((LabeledStatement)node).getLabel();
          if (label.equals("$1")) {
            return false;
          }
        }
        return true;
      }
    });
    assertEquals("[6, 7, 8]", nums.toString());
  }

  void doReplace() {
    ParseTreeNode rootBlock = root.children().get(0);
    assertTrue(rootBlock instanceof Block);
    ParseTreeNode b5 = rootBlock.children().get(1);
    assertTrue(b5 instanceof LabeledStatement);
    assertEquals("$5", ((LabeledStatement) b5).getLabel());

    ((Block) rootBlock).replaceChild(b9, b5);
  }

  public final void testFormatTreePostReplace() throws Exception {
    doReplace();
    StringBuilder sb = new StringBuilder();
    MessageContext mc = new MessageContext();
    root.formatTree(mc, 0, sb);
    assertEquals(
        "LabeledStmtWrapper : $0\n"
        + "  Block\n"
        + "    LabeledStmtWrapper : $1\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 2\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 3\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 4\n"
        + "    LabeledStmtWrapper : $9\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 10\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 11\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 12",
        sb.toString());
  }

  public final void testRenderPostReplace() {
    doReplace();
    StringBuilder sb = new StringBuilder();
    TokenConsumer tc = new JsPrettyPrinter(sb);
    root.render(new RenderContext(tc));
    tc.noMoreTokens();
    assertEquals(
        "$0: {\n"
        + "  $1: {\n"
        + "    2;\n"
        + "    3;\n"
        + "    4;\n"
        + "  }\n"
        + "  $9: {\n"
        + "    10;\n"
        + "    11;\n"
        + "    12;\n"
        + "  }\n"
        + "}",
        sb.toString());
  }

  public final void testVisitPreOrderPostReplace() {
    doReplace();
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 9, 10, 11, 12]", ie.getNums().toString());
  }

  public final void testVisitPreorderDoesntDescendIntoReplaced() {
    IntEnqueuerThatReplaces ie = new IntEnqueuerThatReplaces(5, b9);
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5]", ie.getNums().toString());
  }

  void doInsert(int before) {
    ParseTreeNode rootBlock = root.children().get(0);
    assertTrue(rootBlock instanceof Block);
    ParseTreeNode b;
    switch (before) {
      case -1:
        b = null;
        break;
      case 1:
        b = rootBlock.children().get(0);
        break;
      case 5:
        b = rootBlock.children().get(1);
        break;
      default:
        fail(String.valueOf(before));
        return;
    }
    if (null != b) {
      assertTrue(b instanceof LabeledStatement);
      assertEquals("$" + before, ((LabeledStatement) b).getLabel());
    } else {
      assertEquals(-1, before);
    }

    ((Block) rootBlock).insertBefore(b9, b);
  }

  public final void testFormatTreePostInsert() throws Exception {
    doInsert(5);
    StringBuilder sb = new StringBuilder();
    MessageContext mc = new MessageContext();
    root.formatTree(mc, 0, sb);
    assertEquals(
        "LabeledStmtWrapper : $0\n"
        + "  Block\n"
        + "    LabeledStmtWrapper : $1\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 2\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 3\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 4\n"
        + "    LabeledStmtWrapper : $9\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 10\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 11\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 12\n"
        + "    LabeledStmtWrapper : $5\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 6\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 7\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 8",
        sb.toString());
  }

  public final void testRenderPostInsert() {
    doInsert(5);
    StringBuilder sb = new StringBuilder();
    TokenConsumer tc = new JsPrettyPrinter(sb);
    root.render(new RenderContext(tc));
    tc.noMoreTokens();
    assertEquals(
        "$0: {\n"
        + "  $1: {\n"
        + "    2;\n"
        + "    3;\n"
        + "    4;\n"
        + "  }\n"
        + "  $9: {\n"
        + "    10;\n"
        + "    11;\n"
        + "    12;\n"
        + "  }\n"
        + "  $5: {\n"
        + "    6;\n"
        + "    7;\n"
        + "    8;\n"
        + "  }\n"
        + "}",
        sb.toString());
  }

  public final void testVisitPreOrderPostInsert() {
    doInsert(5);
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 9, 10, 11, 12, 5, 6, 7, 8]",
                 ie.getNums().toString());
  }

  public final void testVisitPreorderDoesntDescendIntoInserted() {
    IntEnqueuerThatInserts ie = new IntEnqueuerThatInserts(5, b9);
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5, 6, 7, 8]",
                 ie.getNums().toString());
  }

  public final void testVisitPostOrderDoesntDescendIntoInserted() {
    IntEnqueuerThatInserts ie = new IntEnqueuerThatInserts(5, b9);
    root.acceptPostOrder(ie, null);
    assertEquals("[2, 3, 4, 1, 6, 7, 8, 5, 0]",
                 ie.getNums().toString());
  }

  public final void testFormatTreePostInsert2() throws Exception {
    doInsert(1);
    StringBuilder sb = new StringBuilder();
    MessageContext mc = new MessageContext();
    root.formatTree(mc, 0, sb);
    assertEquals(
        "LabeledStmtWrapper : $0\n"
        + "  Block\n"
        + "    LabeledStmtWrapper : $9\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 10\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 11\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 12\n"
        + "    LabeledStmtWrapper : $1\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 2\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 3\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 4\n"
        + "    LabeledStmtWrapper : $5\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 6\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 7\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 8",
        sb.toString());
  }

  public final void testRenderPostInsert2() {
    doInsert(1);
    StringBuilder sb = new StringBuilder();
    TokenConsumer tc = new JsPrettyPrinter(sb);
    root.render(new RenderContext(tc));
    tc.noMoreTokens();
    assertEquals(
        "$0: {\n"
        + "  $9: {\n"
        + "    10;\n"
        + "    11;\n"
        + "    12;\n"
        + "  }\n"
        + "  $1: {\n"
        + "    2;\n"
        + "    3;\n"
        + "    4;\n"
        + "  }\n"
        + "  $5: {\n"
        + "    6;\n"
        + "    7;\n"
        + "    8;\n"
        + "  }\n"
        + "}",
        sb.toString());
  }

  public final void testVisitPreOrderPostInsert2() {
    doInsert(1);
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 7, 8]",
                 ie.getNums().toString());
  }

  public final void testVisitPreorderDoesntDescendIntoInserted2() {
    IntEnqueuerThatInserts ie = new IntEnqueuerThatInserts(1, b9);
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5, 6, 7, 8]",
                 ie.getNums().toString());
  }

  public final void testVisitPostOrderDoesntDescendIntoInserted2() {
    IntEnqueuerThatInserts ie = new IntEnqueuerThatInserts(1, b9);
    root.acceptPostOrder(ie, null);
    assertEquals("[2, 3, 4, 1, 6, 7, 8, 5, 0]",
                 ie.getNums().toString());
  }

  public final void testFormatTreePostInsert3() throws Exception {
    doInsert(-1);
    StringBuilder sb = new StringBuilder();
    MessageContext mc = new MessageContext();
    root.formatTree(mc, 0, sb);
    assertEquals(
        "LabeledStmtWrapper : $0\n"
        + "  Block\n"
        + "    LabeledStmtWrapper : $1\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 2\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 3\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 4\n"
        + "    LabeledStmtWrapper : $5\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 6\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 7\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 8\n"
        + "    LabeledStmtWrapper : $9\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 10\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 11\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 12",
        sb.toString());
  }

  public final void testRenderPostInsert3() {
    doInsert(-1);
    StringBuilder sb = new StringBuilder();
    TokenConsumer tc = new JsPrettyPrinter(sb);
    root.render(new RenderContext(tc));
    tc.noMoreTokens();
    assertEquals(
        "$0: {\n"
        + "  $1: {\n"
        + "    2;\n"
        + "    3;\n"
        + "    4;\n"
        + "  }\n"
        + "  $5: {\n"
        + "    6;\n"
        + "    7;\n"
        + "    8;\n"
        + "  }\n"
        + "  $9: {\n"
        + "    10;\n"
        + "    11;\n"
        + "    12;\n"
        + "  }\n"
        + "}",
        sb.toString());
  }

  public final void testVisitPreOrderPostInsert3() {
    doInsert(-1);
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]",
                 ie.getNums().toString());
  }

  void doRemove(int num) {
    ParseTreeNode rootBlock = root.children().get(0);
    assertTrue(rootBlock instanceof Block);
    ParseTreeNode b;
    switch (num) {
      case 1:
        b = rootBlock.children().get(0);
        break;
      case 5:
        b = rootBlock.children().get(1);
        break;
      default:
        fail(String.valueOf(num));
        return;
    }
    assertTrue(b instanceof LabeledStatement);
    assertEquals("$" + num, ((LabeledStatement) b).getLabel());

    ((Block) rootBlock).removeChild(b);
  }

  public final void testFormatTreePostRemove() throws Exception {
    doRemove(5);
    StringBuilder sb = new StringBuilder();
    MessageContext mc = new MessageContext();
    root.formatTree(mc, 0, sb);
    assertEquals(
        "LabeledStmtWrapper : $0\n"
        + "  Block\n"
        + "    LabeledStmtWrapper : $1\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 2\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 3\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 4",
        sb.toString());
  }

  public final void testRenderPostRemove() {
    doRemove(5);
    StringBuilder sb = new StringBuilder();
    TokenConsumer tc = new JsPrettyPrinter(sb);
    root.render(new RenderContext(tc));
    tc.noMoreTokens();
    assertEquals(
        "$0: {\n"
        + "  $1: {\n"
        + "    2;\n"
        + "    3;\n"
        + "    4;\n"
        + "  }\n"
        + "}",
        sb.toString());
  }

  public final void testVisitPreOrderPostRemove() {
    doRemove(5);
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4]", ie.getNums().toString());
  }

  public final void testVisitPostOrderPostRemove() {
    doRemove(5);
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPostOrder(ie, null);
    assertEquals("[2, 3, 4, 1, 0]", ie.getNums().toString());
  }

  public final void testVisitPreorderDoesntDescendIntoRemoved() {
    IntEnqueuerThatRemoves ie = new IntEnqueuerThatRemoves(5);
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5]", ie.getNums().toString());
  }

  public final void testVisitPostOrderDescendsIntoRemoved() {
    IntEnqueuerThatRemoves ie = new IntEnqueuerThatRemoves(5);
    root.acceptPostOrder(ie, null);
    assertEquals("[2, 3, 4, 1, 6, 7, 8, 5, 0]", ie.getNums().toString());
  }

  public final void testFormatTreePostRemove2() throws Exception {
    doRemove(1);
    StringBuilder sb = new StringBuilder();
    MessageContext mc = new MessageContext();
    root.formatTree(mc, 0, sb);
    assertEquals(
        "LabeledStmtWrapper : $0\n"
        + "  Block\n"
        + "    LabeledStmtWrapper : $5\n"
        + "      Block\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 6\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 7\n"
        + "        ExpressionStmt\n"
        + "          IntegerLiteral : 8",
        sb.toString());
  }

  public final void testRenderPostRemove2() {
    doRemove(1);
    StringBuilder sb = new StringBuilder();
    TokenConsumer tc = new JsPrettyPrinter(sb);
    root.render(new RenderContext(tc));
    tc.noMoreTokens();
    assertEquals(
        "$0: {\n"
        + "  $5: {\n"
        + "    6;\n"
        + "    7;\n"
        + "    8;\n"
        + "  }\n"
        + "}",
        sb.toString());
  }

  public final void testVisitPreOrderPostRemove2() {
    doRemove(1);
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 5, 6, 7, 8]", ie.getNums().toString());
  }

  public final void testVisitPostOrderPostRemove2() {
    doRemove(1);
    IntEnqueuer ie = new IntEnqueuer();
    root.acceptPostOrder(ie, null);
    assertEquals("[6, 7, 8, 5, 0]", ie.getNums().toString());
  }

  public final void testVisitPreorderDoesntDescendIntoRemoved2() {
    IntEnqueuerThatRemoves ie = new IntEnqueuerThatRemoves(1);
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 5, 6, 7, 8]", ie.getNums().toString());
  }

  public final void testVisitPostOrderDescendsIntoRemoved2() {
    IntEnqueuerThatRemoves ie = new IntEnqueuerThatRemoves(1);
    root.acceptPostOrder(ie, null);
    assertEquals("[2, 3, 4, 1, 6, 7, 8, 5, 0]", ie.getNums().toString());
  }

  public final void testVisitPostOrderProceedsWhenNextDeleted() {
    doInsert(-1);
    IntEnqueuerThatMungesSiblings ie = new IntEnqueuerThatMungesSiblings(
        6, new long[] { 6, 7 }, new long[0]);
    root.acceptPreOrder(ie, null);
    //   $5: {
    //     6;
    //     7;
    //     8;
    //   }
    assertEquals("[0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12]",
                 ie.getNums().toString());
  }

  public final void testVisitPostOrderProceedsWhenAllNextDeleted1() {
    doInsert(-1);
    IntEnqueuerThatMungesSiblings ie = new IntEnqueuerThatMungesSiblings(
        7, new long[] { 7, 8 }, new long[] { 13 });
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5, 6, 13, 9, 10, 11, 12]",
                 ie.getNums().toString());
  }

  public final void testVisitPostOrderProceedsWhenAllNextDeleted2() {
    doInsert(-1);
    IntEnqueuerThatMungesSiblings ie = new IntEnqueuerThatMungesSiblings(
        6, new long[] { 6, 7, 8 }, new long[0]);
    root.acceptPreOrder(ie, null);
    assertEquals("[0, 1, 2, 3, 4, 5, 9, 10, 11, 12]",
                 ie.getNums().toString());
  }

  public final void testSerializable() throws Exception {
    assertSerializable(root);
  }

  public final void testIssue1369() throws Exception {
    ParseTreeNode p = js(fromString("var x = /asdf/;"));
    assertSerializable(p);
  }

  @SuppressWarnings("unchecked")
  // setComments causes a warning about generic array for varargs
  public final void testImmutability() throws Exception {
    Block n = new Block(
        FilePosition.UNKNOWN,
        Arrays.asList(
            new ExpressionStmt(new IntegerLiteral(FilePosition.UNKNOWN, 42)),
            new ExpressionStmt(new IntegerLiteral(FilePosition.UNKNOWN, 13))));
    assertTrue(n.makeImmutable());
    assertTrue(n.isImmutable());
    assertTrue(n.children().get(0).isImmutable());
    assertTrue(n.children().get(0).children().get(0).isImmutable());
    assertTrue(n.children().get(1).isImmutable());
    assertTrue(n.children().get(1).children().get(0).isImmutable());
    try {
      n.appendChild(new Noop(FilePosition.UNKNOWN));
      fail();
    } catch (UnsupportedOperationException e) { /* OK */ }
    try {
      n.setComments(Arrays.asList(
          Token.instance("test", JsTokenType.COMMENT, FilePosition.UNKNOWN)));
      fail();
    } catch (UnsupportedOperationException e) { /* OK */ }
  }

  private final class AlwaysMutable extends AbstractExpression {
    public AlwaysMutable() { super(FilePosition.UNKNOWN, StringLiteral.class); }
    @Override public Object getValue() { return null; }
    @Override public String typeOf() { return null; }
    @Override public void render(RenderContext r) { /* no output */ }
    @Override public boolean makeImmutable() { return false; /* refuse! */ }
  }

  public final void testIncompleteImmutability() throws Exception {
    // Immutability should be best-effort, stopping at a node that refuses
    Block n = new Block(
        FilePosition.UNKNOWN,
        Arrays.asList(
            new ExpressionStmt(new IntegerLiteral(FilePosition.UNKNOWN, 42)),
            new ExpressionStmt(new AlwaysMutable())));
    assertFalse(n.makeImmutable());
    assertFalse(n.isImmutable());
    assertTrue(n.children().get(0).isImmutable());
    assertTrue(n.children().get(0).children().get(0).isImmutable());
    assertFalse(n.children().get(1).isImmutable());
    assertFalse(n.children().get(1).children().get(0).isImmutable());
  }

  static class IntEnqueuer implements Visitor {
    private final List<Number> nums = new ArrayList<Number>();

    public final Number processNode(ParseTreeNode n) {
      Number num;
      if (n instanceof IntegerLiteral) {
        num = ((IntegerLiteral) n).getValue();
      } else if (n instanceof LabeledStatement) {
        num = Long.valueOf(((LabeledStatement) n).getLabel().substring(1));
      } else {
        return null;
      }
      nums.add(num);
      return num;
    }

    public boolean visit(AncestorChain<?> ancestors) {
      processNode(ancestors.node);
      return true;
    }

    List<Number> getNums() { return nums; }
  }

  static class IntEnqueuerExcept extends IntEnqueuer {
    private final long exception;

    IntEnqueuerExcept(long exception) {
      this.exception = exception;
    }

    @Override
    public boolean visit(AncestorChain<?> ancestors) {
      ParseTreeNode n = ancestors.node;
      Number num = processNode(n);
      return num == null || exception != num.longValue();
    }
  }

  static class IntEnqueuerThatReplaces extends IntEnqueuer {
    private final long toReplace;
    private final ParseTreeNode replacement;

    IntEnqueuerThatReplaces(long toReplace, ParseTreeNode replacement) {
      this.toReplace = toReplace;
      this.replacement = replacement;
    }

    @Override
    public boolean visit(AncestorChain<?> ancestors) {
      ParseTreeNode n = ancestors.node;
      Number num = processNode(n);
      if (null != num && num.longValue() == toReplace) {
        ((MutableParseTreeNode) ancestors.parent.node)
            .replaceChild(replacement, n);
      }
      return true;
    }
  }

  static class IntEnqueuerThatInserts extends IntEnqueuer {
    private final long num;
    private final ParseTreeNode toInsert;

    IntEnqueuerThatInserts(long num, ParseTreeNode toInsert) {
      this.num = num;
      this.toInsert = toInsert;
    }

    @Override
    public boolean visit(AncestorChain<?> ancestors) {
      ParseTreeNode n = ancestors.node;
      Number num = processNode(n);
      if (null != num && num.longValue() == this.num) {
        ((MutableParseTreeNode) ancestors.parent.node)
            .insertBefore(toInsert, n);
      }
      return true;
    }
  }

  static class IntEnqueuerThatRemoves extends IntEnqueuer {
    private final long toRemove;

    IntEnqueuerThatRemoves(long toRemove) {
      this.toRemove = toRemove;
    }

    @Override
    public boolean visit(AncestorChain<?> ancestors) {
      ParseTreeNode n = ancestors.node;
      Number num = processNode(n);
      if (null != num && num.longValue() == toRemove) {
        ((MutableParseTreeNode) ancestors.parent.node).removeChild(n);
      }
      return true;
    }
  }

  static class IntEnqueuerThatMungesSiblings extends IntEnqueuer {
    private final Set<Long> toRemove;
    private final long[] toAdd;
    private final long remover;

    IntEnqueuerThatMungesSiblings(long remover, long[] toRemove, long[] toAdd) {
      this.remover = remover;
      this.toRemove = new HashSet<Long>();
      for (int i = 0; i < toRemove.length; ++i) {
        this.toRemove.add(toRemove[i]);
      }
      this.toAdd = toAdd.clone();
    }

    @Override
    public boolean visit(AncestorChain<?> ancestors) {
      ParseTreeNode n = ancestors.node;
      processNode(n);

      if (n instanceof ExpressionStmt
          && (((IntegerLiteral) n.children().get(0)).getValue().longValue()
              == remover)) {
        MutableParseTreeNode p = (MutableParseTreeNode) ancestors.parent.node;

        List<ParseTreeNode> siblings
            = new ArrayList<ParseTreeNode>(p.children());
        for (ParseTreeNode sibling : siblings) {
          Number num = ((IntegerLiteral) sibling.children().get(0)).getValue();
          if (toRemove.contains(num.longValue())) {
            p.removeChild(sibling);
          }
        }
        for (int i = 0; i < toAdd.length; ++i) {
          p.appendChild(new ExpressionStmt(
              FilePosition.UNKNOWN,
              new IntegerLiteral(FilePosition.UNKNOWN, toAdd[i])));
        }
      }
      return true;
    }
  }
}