// Copyright 2020 The Bazel Authors. 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.devtools.build.lib.query2.engine;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.Argument;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.ArgumentType;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryFunction;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.QueryTaskFuture;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests of parser and pretty-printer. */
@RunWith(JUnit4.class)
public final class QueryParserTest {
  private static class MockFunction implements QueryFunction {
    private final String name;
    private final int mandatoryArguments;
    private final List<ArgumentType> arguments;

    private MockFunction(String name, int mandatoryArguments, ArgumentType... arguments) {
      this.name = name;
      this.mandatoryArguments = mandatoryArguments;
      this.arguments = ImmutableList.copyOf(arguments);
    }

    @Override
    public String getName() {
      return name;
    }

    @Override
    public int getMandatoryArguments() {
      return mandatoryArguments;
    }

    @Override
    public List<ArgumentType> getArgumentTypes() {
      return arguments;
    }

    @Override
    public <T> QueryTaskFuture<Void> eval(
        QueryEnvironment<T> env,
        QueryExpressionContext<T> context,
        QueryExpression expression,
        List<Argument> args,
        Callback<T> callback) {
      throw new IllegalStateException();
    }
  }

  private static QueryEnvironment<?> mockEnvironment() {
    ImmutableList.Builder<QueryFunction> functions = ImmutableList.builder();
    functions.addAll(QueryEnvironment.DEFAULT_QUERY_FUNCTIONS);
    functions.add(new MockFunction("opt", 2,
        ArgumentType.WORD, ArgumentType.WORD, ArgumentType.WORD));

    QueryEnvironment<?> result = mock(QueryEnvironment.class);
    when(result.getFunctions()).thenReturn(functions.build());
    return result;
  }

  // Asserts that 'query' parses, and that when pretty-printed, yields 'query'.
  private static String checkPrettyPrint(String query) throws Exception {
    return checkPrettyPrint(query, query);
  }

  // Asserts that 'query' parses, and that when pretty-printed, yields
  // 'expectedPrettyPrintOutput'.
  private static String checkPrettyPrint(String expectedPrettyPrintOutput, String query)
      throws Exception {
    assertThat(QueryExpression.parse(query, mockEnvironment()).toString())
        .isEqualTo(expectedPrettyPrintOutput);
    return expectedPrettyPrintOutput;
  }

  public static void checkParseFails(String query, String expectedError) {
    QuerySyntaxException e =
        assertThrows(
            QuerySyntaxException.class, () -> QueryExpression.parse(query, mockEnvironment()));
    assertThat(e).hasMessageThat().isEqualTo(expectedError);
  }

  @Test
  public void testOptionalArguments() throws Exception {
    checkPrettyPrint("opt('foo', 'bar')");
    checkPrettyPrint("opt('foo', 'bar', 'qux')");
    checkParseFails("opt('foo', 'bar', 'qux', 'zyc')", "syntax error at ', zyc )'");
    checkParseFails("opt('foo')", "syntax error at ')'");
  }

  @Test
  public void testTargetLiterals() throws Exception {
    checkPrettyPrint("x");
    checkPrettyPrint("//x");
    checkPrettyPrint("//x:y");
    checkPrettyPrint("x/...:all-targets");
    checkPrettyPrint("\"set\""); // reserved word
    checkPrettyPrint("\"\"");
  }

  @Test
  public void checkParseErrors() {
    checkParseFails("foo(a)", "syntax error at '( a )'");
    checkParseFails("deps(", "premature end of input");
    checkParseFails("deps(a, ", "premature end of input");
    checkParseFails("deps(a, b, c, d)", "expected an integer literal: 'b'");
    checkParseFails("set(a, ", "syntax error at ','");
    checkParseFails("set(a b", "premature end of input");
  }

  @Test
  public void testBinaryOperators() throws Exception {
    checkParseFails("foo intersect", "premature end of input");

    checkPrettyPrint("(a - b)", "a - b");

    checkPrettyPrint("(a intersect b)", "a intersect b");
    checkPrettyPrint("(a intersect b intersect c)", "a intersect b intersect c");
    checkPrettyPrint("(a union b)", "a union b");
    checkPrettyPrint("(a union b union c)", "a union b union c");
    checkPrettyPrint("(a except b)", "a except b");
    checkPrettyPrint("(a except b except c)", "a except b except c");
    checkPrettyPrint("((a union b) except c)", "a union b except c");
    checkPrettyPrint("((a except b) union c)", "a except b union c");
  }

  @Test
  public void testOperators() throws Exception {
    checkPrettyPrint("some(x)");
    checkPrettyPrint("somepath(x, y)");
    checkPrettyPrint("allpaths(x, y)");
    checkPrettyPrint("deps(x)");
    checkPrettyPrint("deps(x, 1)");
    checkPrettyPrint("rdeps(x, y)");
    checkPrettyPrint("rdeps(x, y, 1)");
    checkPrettyPrint("kind('rule', x)", "kind(rule, x)");
    checkPrettyPrint("kind('source file', x)");
    checkPrettyPrint("kind('.*', x)");
    checkPrettyPrint("attr('linkshared', '1', x)", "attr(linkshared,1,x)");
    checkPrettyPrint("filter('jar$', x)", "filter(jar$, x)");
    checkPrettyPrint("let x = e1 in e2");
    checkPrettyPrint("labels('srcs', x)");
    checkPrettyPrint("tests(x)");
    checkPrettyPrint("set()");
    checkPrettyPrint("set(//a)");
    checkPrettyPrint("set(//a //b)");
  }

  @Test
  public void testMultipleOperatorParsing() throws Exception {
    checkPrettyPrint(checkPrettyPrint("kind('rule', x)", "kind(rule, x)"));
    checkPrettyPrint(checkPrettyPrint("attr('linkshared', '1', x)", "attr(linkshared,1,x)"));
    checkPrettyPrint(checkPrettyPrint("filter('jar$', x)", "filter(jar$, x)"));
  }

  @Test
  public void testMultipleBinaryOperatorParsing() throws Exception {
    checkPrettyPrint(checkPrettyPrint("((a union b) except c)", "a union b except c"));
    checkPrettyPrint(checkPrettyPrint("(a intersect b intersect c)", "a intersect b intersect c"));
    checkPrettyPrint(checkPrettyPrint("(a union b union c)", "a union b union c"));
    checkPrettyPrint(
        checkPrettyPrint(
            "((((a union b) intersect c) except d) intersect e)",
            "a union b intersect c except d intersect e"));
  }

  @Test
  public void testMultipleTargetLiteralParsing() throws Exception {
    checkPrettyPrint(checkPrettyPrint("//foo:.*@4", "\"//foo:.*@4\""));
    checkPrettyPrint(checkPrettyPrint("set(//foo)", "set(\"//foo\")"));
    checkPrettyPrint("\"set(//foo)\"");
    checkPrettyPrint("\"set('//foo')\"");
  }

  @Test
  public void testQuotedAndUnquotedMetacharacters() throws Exception {
    checkPrettyPrint("\"//foo:xx+xx\"");
    checkPrettyPrint(checkPrettyPrint("(//foo:xx + xx)", "//foo:xx+xx"));
    checkPrettyPrint("\"//foo:xx=xx\"");
    checkParseFails("//foo:xx=xx", "unexpected token '=' after query expression '//foo:xx'");
  }

  @Test
  public void testQuotedSpecialCharacters() throws Exception {
    checkPrettyPrint("\"foo[]^$asd.|asd?*+{})_asd()2\"", "'foo[]^$asd.|asd?*+{})_asd()2'");
    checkPrettyPrint("\"foo[]^$asd.|asd?*+{})_asd()2\"");
    checkPrettyPrint("\" #&()+,;<=>?[]{|}\"");
  }

  @Test
  public void testUnquotedSpecialCharacters() throws Exception {
    // All special characters in the Lexer#scanWord ./@_:~-*$
    checkPrettyPrint("a.b");
    checkPrettyPrint("a/b");
    checkPrettyPrint("a@b");
    checkPrettyPrint("a_b");
    checkPrettyPrint("a:b");
    checkPrettyPrint("a~b");
    checkPrettyPrint("a-b");
    checkPrettyPrint("a*b");
    checkPrettyPrint("a$b");
  }

  @Test
  public void testPreserveQuoting() throws Exception {
    checkPrettyPrint(checkPrettyPrint("\"a+b\""));
    // this should preserve quoting without being quoted in TargetLiteral#toString
    checkPrettyPrint(checkPrettyPrint("aaa", "\"aaa\""));
  }

  @Test
  public void testQuotedIllegalCharacters() throws Exception {
    checkParseFails("\"-x\"", "target literal must not begin with (-): -x");
    checkParseFails("\"*x\"", "target literal must not begin with (*): *x");
  }

  @Test
  public void testIllegalQuoting() throws Exception {
    checkParseFails("\"a", "unclosed quotation");
    checkParseFails("\'a", "unclosed quotation");
    checkParseFails("a\"a", "unclosed quotation");
    checkParseFails("a\'a", "unclosed quotation");
    checkParseFails("a\'\"a", "unclosed quotation");
    checkParseFails("a\"\'a", "unclosed quotation");
    checkParseFails("\'a\"\'a\'", "unclosed quotation");
    checkParseFails("\"a\'\"a\"", "unclosed quotation");
    checkParseFails(
        "\'\"a\" + \'a\'\'", "unexpected token 'a' after query expression ''\"a\" + ''");
    checkParseFails(
        "\"\'a\' + \"a\"\"", "unexpected token 'a' after query expression '\"'a' + \"'");
    checkParseFails(
        "\"set(\"//foo\" + \"bar\")\"",
        "unexpected token '//foo' after query expression '\"set(\"'");
    checkParseFails(
        "'set('//foo' + 'bar')'", "unexpected token '//foo' after query expression '\"set(\"'");
  }

  @Test
  public void testUsingCorrectQuotingInTargetLiteralToString() throws Exception {
    // These tests all fall into the needsQuoting == true use case in TargetLiteral#toString
    checkPrettyPrint("'set(\"//foo\" + \"bar\")'");
    checkPrettyPrint("\"set('//foo' + 'bar')\"");
    checkPrettyPrint("\"a'a\"");
    checkPrettyPrint("\'a\"a\'");
  }
}
