// Copyright (C) 2009 The Android Open Source Project
//
// 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.gerrit.index.query;

import static com.google.gerrit.index.query.QueryParser.AND;
import static com.google.gerrit.index.query.QueryParser.COLON;
import static com.google.gerrit.index.query.QueryParser.DEFAULT_FIELD;
import static com.google.gerrit.index.query.QueryParser.EXACT_PHRASE;
import static com.google.gerrit.index.query.QueryParser.FIELD_NAME;
import static com.google.gerrit.index.query.QueryParser.NOT;
import static com.google.gerrit.index.query.QueryParser.OR;
import static com.google.gerrit.index.query.QueryParser.SINGLE_WORD;
import static com.google.gerrit.index.query.QueryParser.parse;
import static com.google.gerrit.index.query.testing.TreeSubject.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;

import org.antlr.runtime.tree.Tree;
import org.junit.Test;

public class QueryParserTest {
  @Test
  public void fieldNameAndValue() throws Exception {
    Tree r = parse("project:tools/gerrit");
    assertThat(r).hasType(FIELD_NAME);
    assertThat(r).hasText("project");
    assertThat(r).hasChildCount(1);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("tools/gerrit");
    assertThat(r).child(0).hasNoChildren();
  }

  @Test
  public void fieldNameAndValueThatLooksLikeFieldNameColon() throws Exception {
    // This should work, but doesn't due to a known issue.
    assertParseFails("project:foo:");
  }

  @Test
  public void fieldNameAndValueThatLooksLikeFieldNameColonValue() throws Exception {
    Tree r = parse("project:foo:bar");
    assertThat(r).hasType(FIELD_NAME);
    assertThat(r).hasText("project");
    assertThat(r).hasChildCount(3);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("foo");
    assertThat(r).child(0).hasNoChildren();
    assertThat(r).child(1).hasType(COLON);
    assertThat(r).child(1).hasText(":");
    assertThat(r).child(1).hasNoChildren();
    assertThat(r).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(2).hasText("bar");
    assertThat(r).child(2).hasNoChildren();
  }

  @Test
  public void fieldNameAndValueThatLooksLikeWordColonValue() throws Exception {
    Tree r = parse("project:x*y:a*b");
    assertThat(r).hasType(FIELD_NAME);
    assertThat(r).hasText("project");
    assertThat(r).hasChildCount(3);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("x*y");
    assertThat(r).child(0).hasNoChildren();
    assertThat(r).child(1).hasType(COLON);
    assertThat(r).child(1).hasText(":");
    assertThat(r).child(1).hasNoChildren();
    assertThat(r).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(2).hasText("a*b");
    assertThat(r).child(2).hasNoChildren();
  }

  @Test
  public void fieldNameAndValueThatLooksLikeWordColon() throws Exception {
    // This should work, but doesn't due to a known issue.
    assertParseFails("project:x*y:");
  }

  @Test
  public void fieldNameAndValueWithMultipleColons() throws Exception {
    Tree r = parse("project:*:*:*");
    assertThat(r).hasType(FIELD_NAME);
    assertThat(r).hasText("project");
    assertThat(r).hasChildCount(5);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("*");
    assertThat(r).child(0).hasNoChildren();
    assertThat(r).child(1).hasType(COLON);
    assertThat(r).child(1).hasText(":");
    assertThat(r).child(1).hasNoChildren();
    assertThat(r).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(2).hasText("*");
    assertThat(r).child(2).hasNoChildren();
    assertThat(r).child(3).hasType(COLON);
    assertThat(r).child(3).hasText(":");
    assertThat(r).child(3).hasNoChildren();
    assertThat(r).child(4).hasType(SINGLE_WORD);
    assertThat(r).child(4).hasText("*");
    assertThat(r).child(4).hasNoChildren();
  }

  @Test
  public void fieldNameAndValueWithColonFollowedByAnotherField() throws Exception {
    Tree r = parse("project:foo:bar file:baz");
    assertThat(r).hasType(AND);
    assertThat(r).hasChildCount(2);

    assertThat(r).child(0).hasType(FIELD_NAME);
    assertThat(r).child(0).hasText("project");
    assertThat(r).child(0).hasChildCount(3);
    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(0).hasText("foo");
    assertThat(r).child(0).child(0).hasNoChildren();
    assertThat(r).child(0).child(1).hasType(COLON);
    assertThat(r).child(0).child(1).hasText(":");
    assertThat(r).child(0).child(1).hasNoChildren();
    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(2).hasText("bar");
    assertThat(r).child(0).child(2).hasNoChildren();

    assertThat(r).child(1).hasType(FIELD_NAME);
    assertThat(r).child(1).hasText("file");
    assertThat(r).child(1).hasChildCount(1);
    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(1).child(0).hasText("baz");
    assertThat(r).child(1).child(0).hasNoChildren();
  }

  @Test
  public void fieldNameAndValueWithColonFollowedByOpenParen() throws Exception {
    Tree r = parse("project:foo:bar (file:baz)");
    assertThat(r).hasType(AND);
    assertThat(r).hasChildCount(2);

    assertThat(r).child(0).hasType(FIELD_NAME);
    assertThat(r).child(0).hasText("project");
    assertThat(r).child(0).hasChildCount(3);
    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(0).hasText("foo");
    assertThat(r).child(0).child(0).hasNoChildren();
    assertThat(r).child(0).child(1).hasType(COLON);
    assertThat(r).child(0).child(1).hasText(":");
    assertThat(r).child(0).child(1).hasNoChildren();
    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(2).hasText("bar");
    assertThat(r).child(0).child(2).hasNoChildren();

    assertThat(r).child(1).hasType(FIELD_NAME);
    assertThat(r).child(1).hasText("file");
    assertThat(r).child(1).hasChildCount(1);
    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(1).child(0).hasText("baz");
    assertThat(r).child(1).child(0).hasNoChildren();
  }

  @Test
  public void fieldNameAndValueWithColonFollowedByCloseParen() throws Exception {
    Tree r = parse("(project:foo:bar) file:baz");
    assertThat(r).hasType(AND);
    assertThat(r).hasChildCount(2);

    assertThat(r).child(0).hasType(FIELD_NAME);
    assertThat(r).child(0).hasText("project");
    assertThat(r).child(0).hasChildCount(3);
    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(0).hasText("foo");
    assertThat(r).child(0).child(0).hasNoChildren();
    assertThat(r).child(0).child(1).hasType(COLON);
    assertThat(r).child(0).child(1).hasText(":");
    assertThat(r).child(0).child(1).hasNoChildren();
    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(2).hasText("bar");
    assertThat(r).child(0).child(2).hasNoChildren();

    assertThat(r).child(1).hasType(FIELD_NAME);
    assertThat(r).child(1).hasText("file");
    assertThat(r).child(1).hasChildCount(1);
    assertThat(r).child(1).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(1).child(0).hasText("baz");
    assertThat(r).child(1).child(0).hasNoChildren();
  }

  @Test
  public void defaultFieldWithColon() throws Exception {
    Tree r = parse("CodeReview:+2");
    assertThat(r).hasType(DEFAULT_FIELD);
    assertThat(r).hasChildCount(3);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("CodeReview");
    assertThat(r).child(0).hasNoChildren();
    assertThat(r).child(1).hasType(COLON);
    assertThat(r).child(1).hasText(":");
    assertThat(r).child(1).hasNoChildren();
    assertThat(r).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(2).hasText("+2");
    assertThat(r).child(2).hasNoChildren();
  }

  @Test
  public void fieldNameWithEscapedDoubleQuotesInValue() throws Exception {
    // Actual String: A \"special\" word
    String search = "message:\"A \\\"special\\\" word\"";
    Tree r = parse(search);
    assertThat(r).hasType(FIELD_NAME);
    assertThat(r).hasChildCount(1);
    assertThat(r).hasText("message");
    assertThat(r).child(0).hasType(EXACT_PHRASE);
    // Antlr escaped the double quotes in the phrase.
    assertThat(r).child(0).hasText("A \"special\" word");
  }

  @Test
  public void fieldNameWithEscapedTabCharacterIsPreserved() throws Exception {
    String[] searches = {"message:\"A \\t word\"", "message:{A \\t word}"};
    for (String search : searches) {
      Tree r = parse(search);
      assertThat(r).hasType(FIELD_NAME);
      assertThat(r).hasChildCount(1);
      assertThat(r).hasText("message");
      assertThat(r).child(0).hasType(EXACT_PHRASE);
      assertThat(r).child(0).hasText("A \t word");
    }
  }

  @Test
  public void fieldNameWithEscapedBackslashIsIncludedInOutput() throws Exception {
    String search = "message:\"A backslash \\\\ in phrase\"";
    Tree r = parse(search);
    assertThat(r).hasType(FIELD_NAME);
    assertThat(r).hasChildCount(1);
    assertThat(r).hasText("message");
    assertThat(r).child(0).hasType(EXACT_PHRASE);
    assertThat(r).child(0).hasText("A backslash \\ in phrase");
  }

  @Test
  public void upperCaseParsedAsOperators() throws Exception {
    Tree r = parse("project:foo:bar AND file:baz");
    assertThat(r).hasType(AND);
    assertThat(r).hasChildCount(2);

    r = parse("project:foo:bar OR file:baz");
    assertThat(r).hasType(OR);
    assertThat(r).hasChildCount(2);

    r = parse("NOT project:foo:bar");
    assertThat(r).hasType(NOT);
    assertThat(r).hasChildCount(1);
  }

  @Test
  public void lowerCaseParsedAsOperators() throws Exception {
    Tree r = parse("project:foo:bar and file:baz");
    assertThat(r).hasType(AND);
    assertThat(r).hasChildCount(2);

    r = parse("project:foo:bar or file:baz");
    assertThat(r).hasType(OR);
    assertThat(r).hasChildCount(2);

    r = parse("not project:foo:bar");
    assertThat(r).hasType(NOT);
    assertThat(r).hasChildCount(1);
  }

  @Test
  public void fieldNameWithNot() throws Exception {
    Tree r = parse("-foo:bar");
    assertThat(r).hasType(NOT);
    assertThat(r).hasText("NOT");
    assertThat(r).hasChildCount(1);
    assertThat(r).child(0).hasType(FIELD_NAME);
    assertThat(r).child(0).hasText("foo");
    assertThat(r).child(0).hasChildCount(1);
    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(0).hasText("bar");
    assertThat(r).child(0).child(0).hasNoChildren();
  }

  @Test
  public void fieldNameWithDigit() throws Exception {
    Tree r = parse("foo9:bar");
    assertThat(r).hasType(DEFAULT_FIELD);
    assertThat(r).hasChildCount(3);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("foo9");
    assertThat(r).child(0).hasNoChildren();
    assertThat(r).child(1).hasType(COLON);
    assertThat(r).child(1).hasText(":");
    assertThat(r).child(1).hasNoChildren();
    assertThat(r).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(2).hasText("bar");
    assertThat(r).child(2).hasNoChildren();
  }

  @Test
  public void fieldNameWithUnderscore() throws Exception {
    Tree r = parse("foo_bar:baz");
    assertThat(r).hasType(FIELD_NAME);
    assertThat(r).hasText("foo_bar");
    assertThat(r).hasChildCount(1);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("baz");
    assertThat(r).child(0).hasNoChildren();
  }

  @Test
  public void fieldNameWithHyphen() throws Exception {
    Tree r = parse("foo-bar:baz");
    assertThat(r).hasType(FIELD_NAME);
    assertThat(r).hasText("foo-bar");
    assertThat(r).hasChildCount(1);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("baz");
    assertThat(r).child(0).hasNoChildren();
  }

  @Test
  public void fieldNameEndingWithHyphen() throws Exception {
    Tree r = parse("foo-:bar");
    assertThat(r).hasType(DEFAULT_FIELD);
    assertThat(r).hasChildCount(3);
    assertThat(r).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).hasText("foo-");
    assertThat(r).child(0).hasNoChildren();
    assertThat(r).child(1).hasType(COLON);
    assertThat(r).child(1).hasText(":");
    assertThat(r).child(1).hasNoChildren();
    assertThat(r).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(2).hasText("bar");
    assertThat(r).child(2).hasNoChildren();
  }

  @Test
  public void fieldNameWithNotAndHyphen() throws Exception {
    Tree r = parse("-foo-bar:baz");
    assertThat(r).hasType(NOT);
    assertThat(r).hasText("NOT");
    assertThat(r).hasChildCount(1);
    assertThat(r).child(0).hasType(FIELD_NAME);
    assertThat(r).child(0).hasText("foo-bar");
    assertThat(r).child(0).hasChildCount(1);
    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(0).hasText("baz");
    assertThat(r).child(0).child(0).hasNoChildren();
  }

  @Test
  public void fieldNameWithNotAndEndingWithHyphen() throws Exception {
    Tree r = parse("-foo-bar-:baz");
    assertThat(r).hasType(NOT);
    assertThat(r).hasChildCount(1);
    assertThat(r).child(0).hasType(DEFAULT_FIELD);
    assertThat(r).child(0).hasChildCount(3);
    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(0).hasText("foo-bar-");
    assertThat(r).child(0).child(0).hasNoChildren();
    assertThat(r).child(0).child(1).hasType(COLON);
    assertThat(r).child(0).child(1).hasText(":");
    assertThat(r).child(0).child(1).hasNoChildren();
    assertThat(r).child(0).child(2).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(2).hasText("baz");
    assertThat(r).child(0).child(2).hasNoChildren();
  }

  @Test
  public void fieldNameWithMiscellaneousCharacters() throws Exception {
    Tree r = parse("-foo-bar_-baz_:qux");
    assertThat(r).hasType(NOT);
    assertThat(r).hasChildCount(1);
    assertThat(r).child(0).hasType(FIELD_NAME);
    assertThat(r).child(0).hasText("foo-bar_-baz_");
    assertThat(r).child(0).hasChildCount(1);
    assertThat(r).child(0).child(0).hasType(SINGLE_WORD);
    assertThat(r).child(0).child(0).hasText("qux");
    assertThat(r).child(0).child(0).hasNoChildren();
  }

  private static void assertParseFails(String query) {
    assertThrows(QueryParseException.class, () -> parse(query));
  }
}
