/*
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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.turbine.parse;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.common.base.Joiner;
import com.google.turbine.diag.SourceFile;
import com.google.turbine.diag.TurbineError;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for best-effort parser error handling. */
@RunWith(JUnit4.class)
public class ParseErrorTest {

  @Test
  public void intBound() {
    StreamLexer lexer =
        new StreamLexer(
            new UnicodeEscapePreprocessor(new SourceFile("<>", String.valueOf("2147483648"))));
    ConstExpressionParser parser = new ConstExpressionParser(lexer, lexer.next(), lexer.position());
    TurbineError e = assertThrows(TurbineError.class, () -> parser.expression());
    assertThat(e).hasMessageThat().contains("invalid literal");
  }

  @Test
  public void hexIntBound() {
    StreamLexer lexer =
        new StreamLexer(
            new UnicodeEscapePreprocessor(new SourceFile("<>", String.valueOf("0x100000000"))));
    ConstExpressionParser parser = new ConstExpressionParser(lexer, lexer.next(), lexer.position());
    TurbineError e = assertThrows(TurbineError.class, () -> parser.expression());
    assertThat(e).hasMessageThat().contains("invalid literal");
  }

  @Test
  public void unexpectedTopLevel() {
    String input = "public static void main(String[] args) {}";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected token: void",
                "public static void main(String[] args) {}",
                "              ^"));
  }

  @Test
  public void unexpectedIdentifier() {
    String input = "public clas Test {}";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected identifier 'clas'", //
                "public clas Test {}",
                "       ^"));
  }

  @Test
  public void missingTrailingCloseBrace() {
    String input = "public class Test {\n\n";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:2: error: unexpected end of input", //
                "",
                "^"));
  }

  @Test
  public void annotationArgument() {
    String input = "@A(x = System.err.println()) class Test {}\n";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: invalid annotation argument", //
                "@A(x = System.err.println()) class Test {}",
                "                         ^"));
  }

  @Test
  public void dropParens() {
    String input = "enum E { ONE(";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected end of input", //
                "enum E { ONE(",
                "            ^"));
  }

  @Test
  public void dropBlocks() {
    String input = "class T { Object f = new Object() {";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected end of input", //
                "class T { Object f = new Object() {",
                "                                  ^"));
  }

  @Test
  public void unterminatedString() {
    String input = "class T { String s = \"hello\nworld\"; }";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unterminated string literal", //
                "class T { String s = \"hello",
                "                           ^"));
  }

  @Test
  public void emptyChar() {
    String input = "class T { char c = ''; }";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: empty char literal", //
                "class T { char c = ''; }",
                "                    ^"));
  }

  @Test
  public void unterminatedChar() {
    String input = "class T { char c = '; }";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unterminated char literal", //
                "class T { char c = '; }",
                "                     ^"));
  }

  @Test
  public void unterminatedExpr() {
    String input = "class T { String s = hello + world }";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unterminated expression, expected ';' not found", //
                "class T { String s = hello + world }",
                "                     ^"));
  }

  @Test
  public void abruptMultivariableDeclaration() {
    String input = "class T { int x,; }";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: expected token <identifier>", //
                "class T { int x,; }",
                "                ^"));
  }

  @Test
  public void invalidAnnotation() {
    String input = "@Foo(x =  @E [] x) class T {}";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: invalid annotation argument", //
                "@Foo(x =  @E [] x) class T {}",
                "                ^"));
  }

  @Test
  public void unclosedComment() {
    String input = "/** *\u001a/ class Test {}";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unclosed comment", //
                "/** *\u001a/ class Test {}",
                "^"));
  }

  @Test
  public void unclosedGenerics() {
    String input = "enum\te{l;p u@.<@";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected end of input", //
                "enum\te{l;p u@.<@",
                "               ^"));
  }

  @Test
  public void arrayDot() {
    String input = "enum\te{p;ullt[].<~>>>L\0";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected token: <", //
                "enum\te{p;ullt[].<~>>>L\0",
                "                ^"));
  }

  @Test
  public void implementsBeforeExtends() {
    String input = "class T implements A extends B {}";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: 'extends' must come before 'implements'",
                "class T implements A extends B {}",
                "                     ^"));
  }

  @Test
  public void unpairedSurrogate() {
    String input = "import pkg\uD800.PackageTest;";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unpaired surrogate 0xd800",
                "import pkg\uD800.PackageTest;",
                "           ^"));
  }

  @Test
  public void abruptSurrogate() {
    String input = "import pkg\uD800";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines("<>:1: error: unpaired surrogate 0xd800", "import pkg\uD800", "          ^"));
  }

  @Test
  public void unexpectedSurrogate() {
    String input = "..\uD800\uDC00";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected input: U+10000", //
                "..\uD800\uDC00",
                "   ^"));
  }

  @Test
  public void notCast() {
    String input = "@j(@truetugt^(oflur)!%t";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: could not evaluate constant expression",
                "@j(@truetugt^(oflur)!%t",
                "                     ^"));
  }

  @Test
  public void singleLineTextBlockRejected() {
    String input = "class T { String s = \"\"\" \"\"\"; }";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected input: \"",
                "class T { String s = \"\"\" \"\"\"; }",
                "                         ^"));
  }

  @Test
  public void annotationClassLiteral() {
    String input = "@interface A { A value() default @Integer.class; }";
    TurbineError e = assertThrows(TurbineError.class, () -> Parser.parse(input));
    assertThat(e)
        .hasMessageThat()
        .isEqualTo(
            lines(
                "<>:1: error: unexpected token: ;",
                "@interface A { A value() default @Integer.class; }",
                "                                               ^"));
  }

  private static String lines(String... lines) {
    return Joiner.on(System.lineSeparator()).join(lines);
  }
}
