/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.olingo.server.core.uri.parser;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.olingo.commons.api.edm.Edm;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.FullQualifiedName;
import org.apache.olingo.commons.api.edmx.EdmxReference;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriInfoKind;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.UriResourceKind;
import org.apache.olingo.server.api.uri.queryoption.ApplyItem;
import org.apache.olingo.server.api.uri.queryoption.ApplyOption;
import org.apache.olingo.server.api.uri.queryoption.OrderByItem;
import org.apache.olingo.server.api.uri.queryoption.apply.Aggregate;
import org.apache.olingo.server.api.uri.queryoption.apply.AggregateExpression;
import org.apache.olingo.server.api.uri.queryoption.apply.AggregateExpression.StandardMethod;
import org.apache.olingo.server.api.uri.queryoption.apply.BottomTop;
import org.apache.olingo.server.api.uri.queryoption.apply.BottomTop.Method;
import org.apache.olingo.server.api.uri.queryoption.apply.Compute;
import org.apache.olingo.server.api.uri.queryoption.apply.ComputeExpression;
import org.apache.olingo.server.api.uri.queryoption.apply.Concat;
import org.apache.olingo.server.api.uri.queryoption.apply.CustomFunction;
import org.apache.olingo.server.api.uri.queryoption.apply.Expand;
import org.apache.olingo.server.api.uri.queryoption.apply.Filter;
import org.apache.olingo.server.api.uri.queryoption.apply.GroupBy;
import org.apache.olingo.server.api.uri.queryoption.apply.GroupByItem;
import org.apache.olingo.server.api.uri.queryoption.apply.Identity;
import org.apache.olingo.server.api.uri.queryoption.apply.OrderBy;
import org.apache.olingo.server.api.uri.queryoption.apply.Search;
import org.apache.olingo.server.api.uri.queryoption.apply.Skip;
import org.apache.olingo.server.api.uri.queryoption.apply.Top;
import org.apache.olingo.server.api.uri.queryoption.expression.BinaryOperatorKind;
import org.apache.olingo.server.api.uri.queryoption.expression.MethodKind;
import org.apache.olingo.server.core.uri.UriInfoImpl;
import org.apache.olingo.server.core.uri.parser.search.SearchParserException;
import org.apache.olingo.server.core.uri.testutil.ExpandValidator;
import org.apache.olingo.server.core.uri.testutil.FilterTreeToText;
import org.apache.olingo.server.core.uri.testutil.FilterValidator;
import org.apache.olingo.server.core.uri.testutil.ResourceValidator;
import org.apache.olingo.server.core.uri.testutil.TestUriValidator;
import org.apache.olingo.server.core.uri.testutil.TestValidator;
import org.apache.olingo.server.core.uri.validator.UriValidationException;
import org.apache.olingo.server.tecsvc.provider.ComplexTypeProvider;
import org.apache.olingo.server.tecsvc.provider.EdmTechProvider;
import org.apache.olingo.server.tecsvc.provider.EntityTypeProvider;
import org.apache.olingo.server.tecsvc.provider.FunctionProvider;
import org.apache.olingo.server.tecsvc.provider.PropertyProvider;
import org.junit.Test;

/** Tests of the $apply parser inspired by the ABNF test cases. */
public class ApplyParserTest {

  private static final OData odata = OData.newInstance();
  private static final Edm edm = odata.createServiceMetadata(
      new EdmTechProvider(), Collections.<EdmxReference> emptyList()).getEdm();

  @Test
  public void basic() throws Exception {
    parseEx("ESTwoKeyNav", "").isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESAllPrim(0)/PropertyInt16", "identity")
        .isExValidation(UriValidationException.MessageKeys.SYSTEM_QUERY_OPTION_NOT_ALLOWED);
  }

  @Test
  public void aggregate() throws Exception {
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 with sum as s)")
        .is(Aggregate.class)
        .goAggregate(0).isStandardMethod(StandardMethod.SUM).isAlias("s")
        .goExpression().goPath().first().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 with min as m)")
        .goAggregate(0).isStandardMethod(StandardMethod.MIN).isAlias("m");
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 with max as m)")
        .goAggregate(0).isStandardMethod(StandardMethod.MAX).isAlias("m");
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 with average as a)")
        .goAggregate(0).isStandardMethod(StandardMethod.AVERAGE).isAlias("a");
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 with countdistinct as c)")
        .goAggregate(0).isStandardMethod(StandardMethod.COUNT_DISTINCT).isAlias("c");
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 with custom.aggregate as c)")
        .is(Aggregate.class)
        .goAggregate(0).isCustomMethod(new FullQualifiedName("custom", "aggregate")).isAlias("c");
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 with min as min,PropertyInt16 with max as max)")
        .goAggregate(0).isStandardMethod(StandardMethod.MIN).isAlias("min").goUp()
        .goAggregate(1).isStandardMethod(StandardMethod.MAX).isAlias("max");
    
    parseEx("ESTwoKeyNav", "aggregate()")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESTwoKeyNav", "aggregate(PropertyInt16 with sum)")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESTwoKeyNav", "aggregate(PropertyInt16 as s)")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESTwoKeyNav", "aggregate(PropertyInt16 with SUM as s)")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESTwoKeyNav", "aggregate(PropertyString with countdistinct as PropertyInt16)")
        .isExSemantic(UriParserSemanticException.MessageKeys.IS_PROPERTY);
    parseEx("ESTwoKeyNav", "aggregate(PropertyInt16 with min as m,PropertyInt16 with max as m)")
        .isExSemantic(UriParserSemanticException.MessageKeys.IS_PROPERTY);
  }

  @Test
  public void customAggregate() throws Exception {
	    parse("ESTwoKeyNav", "aggregate(customAggregate)")
        .is(Aggregate.class)
        .goAggregate(0)
   	    .noExpression()
   	    .noInlineAggregateExpression()
        .goPath().first().isUriPathInfoKind(UriResourceKind.primitiveProperty);
  }

  @Test
  public void customAggregateNamedAsProperty() throws Exception {
	  parse("ESTwoKeyNav", "aggregate(PropertyInt16)")
	  .is(Aggregate.class)
	  .goAggregate(0)
	  .noExpression()
	  .noInlineAggregateExpression()
	  .goPath().first().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
  }

  @Test
  public void aggregateExpression() throws Exception {
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 mul PropertyComp/PropertyInt16 with sum as s)")
        .is(Aggregate.class)
        .goAggregate(0).isStandardMethod(StandardMethod.SUM)
        .goExpression().isBinary(BinaryOperatorKind.MUL)
        .left().goPath().first().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false)
        .goUpFilterValidator().root()
        .right().goPath().first().isComplexProperty("PropertyComp", ComplexTypeProvider.nameCTPrimComp, false)
        .n().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);

    parse("ESTwoKeyNav",
        "aggregate(NavPropertyETKeyNavMany(PropertyInt16 mul NavPropertyETTwoKeyNavOne/PropertyInt16 with sum as s))")
        .goAggregate(0)
        .goInlineAggregateExpression().isStandardMethod(StandardMethod.SUM)
        .goUpAggregate()
        .goPath().first().isNavProperty("NavPropertyETKeyNavMany", EntityTypeProvider.nameETKeyNav, true);

    parseEx("ESTwoKeyNav", "aggregate((PropertyInt16 mul 2 with sum as s))")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
  }

  @Test
  public void aggregateCount() throws Exception {
    parse("ESTwoKeyNav", "aggregate($count as count)")
        .is(Aggregate.class)
        .goAggregate(0).goPath().first().isCount();

    parseEx("ESTwoKeyNav", "aggregate($count)").isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESTwoKeyNav", "aggregate($count with sum as count)")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
  }

  @Test
  public void aggregateFrom() throws Exception {
    parse("ESTwoKeyNav", "aggregate(PropertyInt16 with sum as s from CollPropertyComp with average)")
        .goAggregate(0).isStandardMethod(StandardMethod.SUM)
        .goFrom(0).isStandardMethod(StandardMethod.AVERAGE)
        .goExpression().goPath().first()
        .isComplexProperty("CollPropertyComp", ComplexTypeProvider.nameCTPrimComp, true);
    parse("ESTwoKeyNav",
        "aggregate(PropertyInt16 with sum as s from CollPropertyComp with average from CollPropertyString with max)")
        .goAggregate(0).isStandardMethod(StandardMethod.SUM)
        .goFrom(0).isStandardMethod(StandardMethod.AVERAGE)
        .goUpAggregate().goFrom(1).isStandardMethod(StandardMethod.MAX);
    parse("ESTwoKeyNav", "aggregate(customAggregate as a from CollPropertyComp with average)")
        .goAggregate(0).goFrom(0).isStandardMethod(StandardMethod.AVERAGE);

    parseEx("ESTwoKeyNav", "aggregate(PropertyInt16 as a from CollPropertyComp with average)")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESTwoKeyNav", "aggregate(PropertyInt16 with sum from CollPropertyComp with average)")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
  }

  @Test
  public void identity() throws Exception {
    parse("ESTwoKeyNav", "identity").is(Identity.class);

    parseEx("ESTwoKeyNav", "identity()")
        .isExSyntax(UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION);
  }

  @Test
  public void compute() throws Exception {
    parse("ESTwoKeyNav", "compute(PropertyInt16 mul NavPropertyETKeyNavOne/PropertyInt16 as p)")
        .is(Compute.class)
        .goCompute(0).isAlias("p").goExpression().isBinary(BinaryOperatorKind.MUL)
        .left().isMember().goPath().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
    parse("ESTwoKeyNav", "compute(PropertyInt16 mul 2 as p,day(now()) as d)")
        .goCompute(0).isAlias("p")
        .goUp().goCompute(1).isAlias("d")
        .goExpression().isMethod(MethodKind.DAY, 1).goParameter(0).isMethod(MethodKind.NOW, 0);

    parseEx("ESTwoKeyNav", "compute(PropertyInt16)")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESTwoKeyNav", "compute(PropertyComp as c)")
        .isExSemantic(UriParserSemanticException.MessageKeys.ONLY_FOR_PRIMITIVE_TYPES);
  }

  @Test
  public void concat() throws Exception {
    parse("ESTwoKeyNav", "concat(topcount(2,PropertyInt16),bottomcount(2,PropertyInt16))")
        .is(Concat.class)
        .goConcat(0).goBottomTop().isMethod(Method.TOP_COUNT)
        .goUp().goUp()
        .goConcat(1).goBottomTop().isMethod(Method.BOTTOM_COUNT).goNumber().isLiteral("2");

    parseEx("ESTwoKeyNav", "concat(identity)").isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    
    parse("ESTwoKeyNav", "concat("
    		+ "groupby((PropertyString),aggregate(PropertyInt16 with sum as s)),"
    		+ "groupby((PropertyString,PropertyInt16),aggregate(PropertyInt16 with sum as s1))"
    		+ "/orderby(PropertyString,PropertyInt16)"
    		+ "/skip(3)"
    		+ "/top(10))")
    .is(Concat.class)
    .goConcat(0).goGroupBy(0).goUp().goUp()
    .goConcat(1).goGroupBy(0).goUp().goUp()
    .goConcat(1).at(1).goOrderBy().goValue("<false<PropertyString>,false<PropertyInt16>>").goUp().goUp()
    .goConcat(1).at(2).goSkip().goValue(3).goUp().goUp()
    .goConcat(1).at(3).goTop().goValue(10).goUp().goUp();
    
    parse("ESTwoKeyNav", "groupby((PropertyString,PropertyInt16),aggregate(PropertyInt16 with sum as s1))"
    		+ "/orderby(PropertyString asc,PropertyInt16,NavPropertyETTwoKeyNavOne/PropertyString desc)"
    		+ "/skip(3)"
    		+ "/top(10)/concat(identity,aggregate(PropertyInt16 with max as s2))")
    .at(0).goGroupBy(0).goUp()
    .at(1).goOrderBy().goValue("<false<PropertyString>,false<PropertyInt16>"
    		+ ",true<NavPropertyETTwoKeyNavOne/PropertyString>>").goUp()
    .at(2).goSkip().goValue(3).goUp()
    .at(3).goTop().goValue(10).goUp()
    .at(4).is(Concat.class);
  }

  @Test
  public void expand() throws Exception {
    parse("ESTwoKeyNav", "expand(NavPropertyETKeyNavMany,filter(PropertyInt16 gt 2))")
        .is(Expand.class).goExpand()
        .goPath().first().isNavProperty("NavPropertyETKeyNavMany", EntityTypeProvider.nameETKeyNav, true)
        .goUpExpandValidator().goFilter().is("<<PropertyInt16> gt <2>>");
    parse("ESTwoKeyNav",
        "expand(NavPropertyETKeyNavMany,expand(NavPropertyETTwoKeyNavMany,filter(PropertyInt16 gt 2)))")
        .is(Expand.class).goExpand().goExpand().goFilter().is("<<PropertyInt16> gt <2>>");
    parse("ESTwoKeyNav",
        "expand(NavPropertyETKeyNavMany,expand(NavPropertyETTwoKeyNavMany,filter(PropertyInt16 gt 2)),"
        + "expand(NavPropertyETTwoKeyNavOne,expand(NavPropertyETKeyNavMany)))")
        .is(Expand.class).goExpand().goExpand().next().goExpand()
        .goPath().first().isNavProperty("NavPropertyETKeyNavMany", EntityTypeProvider.nameETKeyNav, true);

    parseEx("ESTwoKeyNav", "expand()")
        .isExSemantic(UriParserSemanticException.MessageKeys.EXPRESSION_PROPERTY_NOT_IN_TYPE);
  }

  @Test
  public void search() throws Exception {
    parse("ESTwoKeyNav", "search(String)").isSearch("'String'");

    parseEx("ESTwoKeyNav", "search()")
        .isExceptionMessage(SearchParserException.MessageKeys.EXPECTED_DIFFERENT_TOKEN);
  }

  @Test
  public void filter() throws Exception {
    parse("ESTwoKeyNav", "filter(PropertyInt16 gt 3)")
        .is(Filter.class)
        .goFilter().isBinary(BinaryOperatorKind.GT)
        .left().isMember().goPath().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);

    parseEx("ESTwoKeyNav", "filter()").isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
  }

  @Test
  public void bottomTop() throws Exception {
    parse("ESTwoKeyNav", "topcount(2,PropertyInt16)")
        .goBottomTop().isMethod(Method.TOP_COUNT)
        .goNumber().isLiteralType(odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.SByte))
        .isLiteral("2");
    parse("ESTwoKeyNav", "topsum(2,PropertyInt16)")
        .goBottomTop().isMethod(Method.TOP_SUM)
        .goValue().isMember().goPath().first().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
    parse("ESTwoKeyNav", "toppercent(2,PropertyInt16)").goBottomTop().isMethod(Method.TOP_PERCENT);

    parse("ESTwoKeyNav", "bottomcount(2,PropertyInt16)").goBottomTop().isMethod(Method.BOTTOM_COUNT);
    parse("ESTwoKeyNav", "bottomsum(2,PropertyInt16)").goBottomTop().isMethod(Method.BOTTOM_SUM);
    parse("ESTwoKeyNav", "bottompercent(2,PropertyInt16)").goBottomTop().isMethod(Method.BOTTOM_PERCENT);

    parseEx("ESTwoKeyNav", "bottompercent(1.2,PropertyInt16)")
        .isExSemantic(UriParserSemanticException.MessageKeys.TYPES_NOT_COMPATIBLE);
    parseEx("ESTwoKeyNav", "bottompercent(2,PropertyString)")
        .isExSemantic(UriParserSemanticException.MessageKeys.TYPES_NOT_COMPATIBLE);
  }

  @Test
  public void customFunction() throws Exception {
    parse("ESBaseTwoKeyNav", "Namespace1_Alias.BFCESBaseTwoKeyNavRTESBaseTwoKey()")
        .isCustomFunction(FunctionProvider.nameBFCESBaseTwoKeyNavRTESBaseTwoKey);
    parse("ESKeyNav(1)/CollPropertyComp", "Namespace1_Alias.BFCCollCTPrimCompRTESAllPrim()")
        .isCustomFunction(FunctionProvider.nameBFCCollCTPrimCompRTESAllPrim);

    parseEx("ESBaseTwoKeyNav", "BFCESBaseTwoKeyNavRTESBaseTwoKey()")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESBaseTwoKeyNav", "Namespace1_Alias.BFCETBaseTwoKeyNavRTETTwoKeyNav()")
        .isExSemantic(UriParserSemanticException.MessageKeys.FUNCTION_NOT_FOUND);
    parseEx("ESBaseTwoKeyNav", "Namespace1_Alias.BFCCollStringRTESTwoKeyNav()")
        .isExSemantic(UriParserSemanticException.MessageKeys.FUNCTION_NOT_FOUND);
    parseEx("ESTwoKeyNav", "Namespace1_Alias.BFCESTwoKeyNavRTTwoKeyNav()")
        .isExSemantic(UriParserSemanticException.MessageKeys.FUNCTION_MUST_USE_COLLECTIONS);
  }

  @Test
  public void groupBy() throws Exception {
    parse("ESTwoKeyNav", "groupby((PropertyString))")
        .is(GroupBy.class)
        .goGroupBy(0).goPath().first().isPrimitiveProperty("PropertyString", PropertyProvider.nameString, false);
    parse("ESTwoKeyNav", "groupby((NavPropertyETKeyNavOne/PropertyInt16))")
        .is(GroupBy.class)
        .goGroupBy(0).goPath().first().isNavProperty("NavPropertyETKeyNavOne", EntityTypeProvider.nameETKeyNav, false)
        .n().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
    parse("ESTwoKeyNav", "groupby((NavPropertyETKeyNavOne/PropertyInt16,PropertyString))")
        .is(GroupBy.class)
        .goGroupBy(1).goPath().first().isPrimitiveProperty("PropertyString", PropertyProvider.nameString, false);
    parse("ESTwoKeyNav", "groupby((NavPropertyETKeyNavOne/PropertyInt16,NavPropertyETKeyNavOne/PropertyString))");
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne/PropertyInt16,NavPropertyETKeyNavOne/PropertyString,PropertyString))")
        .goGroupBy(2).goPath().first().isPrimitiveProperty("PropertyString", PropertyProvider.nameString, false);

    parse("ESTwoKeyNav", "groupby((Namespace1_Alias.ETBaseTwoKeyNav/NavPropertyETBaseTwoKeyNavOne/PropertyInt16))")
        .is(GroupBy.class)
        .goGroupBy(0).goPath().first().isType(EntityTypeProvider.nameETBaseTwoKeyNav)
        .n().isNavProperty("NavPropertyETBaseTwoKeyNavOne", EntityTypeProvider.nameETBaseTwoKeyNav, false)
        .n().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
    parse("ESTwoKeyNav", "groupby((NavPropertyETTwoKeyNavOne/Namespace1_Alias.ETBaseTwoKeyNav/PropertyInt16))")
        .is(GroupBy.class)
        .goGroupBy(0).goPath()
        .first().isNavProperty("NavPropertyETTwoKeyNavOne", EntityTypeProvider.nameETTwoKeyNav, false)
        .isTypeFilterOnCollection(EntityTypeProvider.nameETBaseTwoKeyNav)
        .n().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);

    parseEx("ESTwoKeyNav", "groupby((wrongProperty))")
        .isExSemantic(UriParserSemanticException.MessageKeys.EXPRESSION_PROPERTY_NOT_IN_TYPE);
    parseEx("ESTwoKeyNav", "groupby((Namespace1_Alias.ETBaseTwoKeyNav))")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
    parseEx("ESTwoKeyNav", "groupby((NavPropertyETTwoKeyNavOne/Namespace1_Alias.ETBaseTwoKeyNav))")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
  }

  @Test
  public void groupByAggregate() throws Exception {
    parse("ESTwoKeyNav", "groupby((PropertyInt16),aggregate(PropertyInt16 with sum as s))")
        .goGroupByOption().goAggregate(0).isStandardMethod(StandardMethod.SUM)
        .goExpression().goPath().first().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne/PropertyInt16),aggregate(PropertyInt16 with average as a))")
        .goGroupByOption().goAggregate(0).isStandardMethod(StandardMethod.AVERAGE)
        .goUp().goUp().goGroupBy(0).goPath()
        .first().isNavProperty("NavPropertyETKeyNavOne", EntityTypeProvider.nameETKeyNav, false)
        .n().isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
    parse("ESTwoKeyNav", "groupby((NavPropertyETKeyNavOne/PropertyInt16),"
        + "aggregate(PropertyInt16 with sum as s,PropertyInt16 with average as a))")
        .goGroupByOption().goAggregate(1).isStandardMethod(StandardMethod.AVERAGE);
    parse("ESTwoKeyNav", "groupby((PropertyInt16),aggregate(NavPropertyETKeyNavMany/$count as c))")
        .goGroupByOption().goAggregate(0).goPath().at(1).isCount();
    parse("ESTwoKeyNav", "groupby((PropertyString),aggregate(NavPropertyETKeyNavMany(PropertyInt16 with sum as s)))")
        .goGroupByOption().goAggregate(0).goInlineAggregateExpression().isStandardMethod(StandardMethod.SUM)
        .goUpAggregate().goPath().first()
        .isNavProperty("NavPropertyETKeyNavMany", EntityTypeProvider.nameETKeyNav, true);
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne/PropertyInt16,NavPropertyETKeyNavOne/PropertyString),"
        + "aggregate(PropertyInt16 with sum as s))")
        .goGroupBy(1).goPath().at(1).isPrimitiveProperty("PropertyString", PropertyProvider.nameString, false);
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne/PropertyInt16,NavPropertyETKeyNavOne/PropertyString),"
        + "aggregate(PropertyInt16 with sum as s from NavPropertyETKeyNavOne/PropertyInt16 with average))")
        .goGroupByOption().goAggregate(0).goFrom(0).isStandardMethod(StandardMethod.AVERAGE);
    parse("ESTwoKeyNav", "groupby((NavPropertyETKeyNavOne),aggregate(CollPropertyComp(PropertyInt16 with sum as s)))");
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETTwoKeyNavOne/PropertyInt16),"
        + "topcount(2,PropertyInt16)/aggregate(PropertyInt16 with sum as s))")
        .goGroupByOption()
        .at(0).goBottomTop().isMethod(Method.TOP_COUNT)
        .goUp().at(1).goAggregate(0).isStandardMethod(StandardMethod.SUM);

    parseEx("ESTwoKeyNav", "groupby((PropertyInt16),identity,identity)")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
  }

  @Test
  public void groupByRollUp() throws Exception {
    parse("ESTwoKeyNav",
        "groupby((rollup(NavPropertyETKeyNavOne/PropertyInt16,NavPropertyETKeyNavOne/PropertyString),"
        + "rollup(NavPropertyETKeyNavOne/NavPropertyETTwoKeyNavOne/PropertyInt16,"
        + "NavPropertyETTwoKeyNavOne/PropertyString),NavPropertyETTwoKeyNavOne/PropertyInt16),"
        + "aggregate(PropertyInt16 with sum as s))")
        .goGroupBy(1).goRollup(1).goPath()
        .first().isNavProperty("NavPropertyETTwoKeyNavOne", EntityTypeProvider.nameETTwoKeyNav, false)
        .n().isPrimitiveProperty("PropertyString", PropertyProvider.nameString, false);

    parse("ESTwoKeyNav",
        "groupby((rollup($all,NavPropertyETKeyNavOne/PropertyInt16,NavPropertyETKeyNavOne/PropertyString),"
        + "NavPropertyETTwoKeyNavOne/PropertyString),"
        + "aggregate(PropertyInt16 with sum as s from NavPropertyETTwoKeyNavOne/PropertyInt16 with average "
        + "from NavPropertyETTwoKeyNavOne/PropertyString with average))")
        .goGroupBy(0).isRollupAll().goUp().goGroupByOption().goAggregate(0).goFrom(1)
        .isStandardMethod(StandardMethod.AVERAGE).goExpression().goPath().at(1)
        .isPrimitiveProperty("PropertyString", PropertyProvider.nameString, false);

    parseEx("ESTwoKeyNav", "groupby((rollup($all)))")
        .isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
  }

  @Test
  public void groupBySpecial() throws Exception {
    parse("ESTwoKeyNav", "groupby((NavPropertyETTwoKeyNavOne/PropertyInt16),aggregate(customAggregate))")
        .is(GroupBy.class)
        .goGroupByOption().goAggregate(0)
        .goPath().first().isUriPathInfoKind(UriResourceKind.primitiveProperty);

    parse("ESTwoKeyNav",
        "groupby((PropertyString),aggregate(NavPropertyETKeyNavMany/$count as c,"
        + "NavPropertyETKeyNavMany(PropertyInt16 with sum as s)))")
        .is(GroupBy.class)
        .goGroupByOption().goAggregate(0).isAlias("c").goPath().at(1).isCount();
    parse("ESTwoKeyNav",
        "groupby((PropertyString),aggregate(NavPropertyETKeyNavMany($count as c),"
        + "NavPropertyETKeyNavMany(PropertyInt16 with sum as s)))")
        .is(GroupBy.class)
        .goGroupByOption().goAggregate(0).goInlineAggregateExpression()
        .isAlias("c").goPath().first().isCount();
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne/PropertyString),"
        + "aggregate(PropertyInt16 with sum as s,customAggregate))")
        .is(GroupBy.class)
        .goGroupByOption().goAggregate(1).isStandardMethod(null).isAlias(null)
        .goPath().first().isUriPathInfoKind(UriResourceKind.primitiveProperty);
    parse("ESTwoKeyNav",
        "groupby((PropertyString),aggregate(NavPropertyETKeyNavMany(PropertyInt16 with sum as s),"
        + "NavPropertyETKeyNavMany/customAggregate))")
        .is(GroupBy.class)
        .goGroupByOption().goAggregate(1)
        .goPath().at(1).isUriPathInfoKind(UriResourceKind.primitiveProperty);
    parse("ESTwoKeyNav",
        "groupby((PropertyString),aggregate(NavPropertyETKeyNavMany(PropertyInt16 with sum as s),"
        + "NavPropertyETKeyNavMany(PropertyInt16 with average as a)))")
        .is(GroupBy.class)
        .goGroupByOption().goAggregate(0).goInlineAggregateExpression()
        .isStandardMethod(StandardMethod.SUM).isAlias("s")
        .goUpAggregate().goUp().goAggregate(1).goInlineAggregateExpression()
        .isStandardMethod(StandardMethod.AVERAGE).isAlias("a");
  }

  @Test
  public void sequence() throws Exception {
    parse("ESTwoKeyNav", "identity/identity/identity")
        .at(0).is(Identity.class).at(1).is(Identity.class).at(2).is(Identity.class);

    parse("ESTwoKeyNav", "filter(PropertyInt16 le 1)/aggregate(PropertyInt16 with sum as s)")
        .at(0).is(Filter.class)
        .at(1).is(Aggregate.class).goAggregate(0).isStandardMethod(StandardMethod.SUM).isAlias("s");
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne),aggregate(PropertyInt16 with sum as s))/"
        + "aggregate(s with average as a)")
        .at(1).goAggregate(0).isStandardMethod(StandardMethod.AVERAGE).isAlias("a");
    parse("ESTwoKeyNav",
        "filter(PropertyInt16 ge 1)/"
        + "groupby((NavPropertyETKeyNavOne/PropertyString),aggregate(PropertyInt16 with sum as s))")
        .at(0).is(Filter.class)
        .at(1).is(GroupBy.class);
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne/PropertyString),aggregate(PropertyInt16 with sum as s))/"
        + "filter(s ge 10)/concat(identity,groupby((NavPropertyETKeyNavOne/PropertyString),"
        + "aggregate(s with sum as t)))")
        .at(0).is(GroupBy.class)
        .at(1).is(Filter.class)
        .at(2).is(Concat.class).goConcat(0).is(Identity.class);
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne/PropertyString),aggregate(PropertyInt16 with sum as s))/"
        + "filter(s ge 10)/groupby((rollup(NavPropertyETKeyNavOne/PropertyString,"
        + "NavPropertyETKeyNavOne/PropertyCompAllPrim/PropertyDuration)),aggregate(s with sum as t))")
        .at(0).is(GroupBy.class)
        .at(1).is(Filter.class)
        .at(2).is(GroupBy.class).goGroupBy(0).goRollup(1).goPath().at(2)
        .isPrimitiveProperty("PropertyDuration", PropertyProvider.nameDuration, false);
    parse("ESTwoKeyNav",
        "groupby((NavPropertyETKeyNavOne/PropertyString),aggregate(PropertyInt16 with sum as s))/"
        + "concat(filter(s ge 10),groupby((NavPropertyETKeyNavOne/PropertyString),"
        + "aggregate(s with sum as t)))")
        .at(0).is(GroupBy.class)
        .at(1).is(Concat.class).goConcat(1).is(GroupBy.class);

    parse("ESTwoKeyNav",
        "filter(PropertyInt16 eq 1)/expand(NavPropertyETKeyNavMany,filter(not PropertyCompAllPrim/PropertyBoolean))/"
        + "groupby((NavPropertyETKeyNavOne/PropertyInt16),"
        + "aggregate(NavPropertyETKeyNavMany(PropertyInt16 with sum as s)))")
        .at(0).is(Filter.class)
        .at(1).is(Expand.class)
        .at(2).is(GroupBy.class);

    parseEx("ESTwoKeyNav", "identity/").isExSyntax(UriParserSyntaxException.MessageKeys.SYNTAX);
  }

  @Test
  public void otherQueryOptions() throws Exception {
    new TestUriValidator().setEdm(edm).run("ESTwoKeyNav",
        "$apply=aggregate(PropertyInt16 with sum as s)&$filter=s gt 3&$select=s")
        .goSelectItemPath(0).first().isPrimitiveProperty("s", PropertyProvider.nameDecimal, false)
        .goUpUriValidator()
        .goFilter().left().goPath().first().isPrimitiveProperty("s", PropertyProvider.nameDecimal, false);

    new FilterValidator().setEdm(edm).runUriOrderBy("ESTwoKeyNav",
        "$apply=aggregate(PropertyInt16 with sum as s)&$orderby=s")
        .goOrder(0).goPath().first().isPrimitiveProperty("s", PropertyProvider.nameDecimal, false);
  }

  @Test
  public void onCount() throws Exception {
    parse("ESTwoKeyNav/$count", "aggregate(PropertyInt16 with sum as s)")
        .goAggregate(0).isStandardMethod(StandardMethod.SUM).isAlias("s")
        .goExpression().goPath().first()
        .isPrimitiveProperty("PropertyInt16", PropertyProvider.nameInt16, false);
  }

  private ApplyValidator parse(final String path, final String apply)
      throws UriParserException, UriValidationException {
    final UriInfo uriInfo = new Parser(edm, odata).parseUri(path, "$apply=" + apply, null, null);
    return new ApplyValidator(uriInfo.getApplyOption());
  }

  private TestUriValidator parseEx(final String path, final String apply) {
    return new TestUriValidator().setEdm(edm).runEx(path, "$apply=" + apply);
  }

  private final class ApplyValidator implements TestValidator {

    private final ApplyOption applyOption;
    private final ApplyValidator previous;
    private ApplyItem applyItem;

    protected ApplyValidator(final ApplyOption applyOption) {
      this(applyOption, null);
    }

    private ApplyValidator(final ApplyOption applyOption, final ApplyValidator previous) {
      this.applyOption = applyOption;
      this.previous = previous;
      at(0);
    }

    public ApplyValidator at(final int index) {
      assertTrue(index < applyOption.getApplyItems().size());
      applyItem = applyOption.getApplyItems().get(index);
      return this;
    }

    public ApplyValidator is(final Class<? extends ApplyItem> cls) {
      assertNotNull(applyItem);
      assertTrue(cls.isAssignableFrom(applyItem.getClass()));
      return this;
    }

    public AggregateValidator goAggregate(final int index) {
      is(Aggregate.class);
      assertTrue(index < ((Aggregate) applyItem).getExpressions().size());
      return new AggregateValidator(((Aggregate) applyItem).getExpressions().get(index), this);
    }

    public ExpandValidator goExpand() {
      is(Expand.class);
      return new ExpandValidator().setUpValidator(this).setExpand(((Expand) applyItem).getExpandOption());
    }

    public FilterValidator goFilter() {
      is(Filter.class);
      return new FilterValidator().setFilter(((Filter) applyItem).getFilterOption());
    }

    public BottomTopValidator goBottomTop() {
      is(BottomTop.class);
      return new BottomTopValidator((BottomTop) applyItem, this);
    }
    
    public OrderByValidator goOrderBy() {
        is(OrderBy.class);
        return new OrderByValidator((OrderBy) applyItem, this);
      }
    
    public TopValidator goTop() {
        is(Top.class);
        return new TopValidator((Top) applyItem, this);
      }
    
    public SkipValidator goSkip() {
        is(Skip.class);
        return new SkipValidator((Skip) applyItem, this);
      }

    public ApplyValidator isCustomFunction(final FullQualifiedName function) {
      is(CustomFunction.class);
      assertEquals(function, ((CustomFunction) applyItem).getFunction().getFullQualifiedName());
      return this;
    }

    public ApplyValidator isSearch(final String serializedSearch) {
      is(Search.class);
      assertEquals(serializedSearch, ((Search) applyItem).getSearchOption().getSearchExpression().toString());
      return this;
    }

    public ApplyValidator goConcat(final int index) {
      is(Concat.class);
      assertTrue(index < ((Concat) applyItem).getApplyOptions().size());
      return new ApplyValidator(((Concat) applyItem).getApplyOptions().get(index), this);
    }

    public ComputeValidator goCompute(final int index) {
      is(Compute.class);
      assertTrue(index < ((Compute) applyItem).getExpressions().size());
      return new ComputeValidator(((Compute) applyItem).getExpressions().get(index), this);
    }

    public GroupByValidator goGroupBy(final int index) {
      is(GroupBy.class);
      assertTrue(index < ((GroupBy) applyItem).getGroupByItems().size());
      return new GroupByValidator(((GroupBy) applyItem).getGroupByItems().get(index), this);
    }

    public ApplyValidator goGroupByOption() {
      is(GroupBy.class);
      assertNotNull(((GroupBy) applyItem).getApplyOption());
      return new ApplyValidator(((GroupBy) applyItem).getApplyOption(), this);
    }

    public ApplyValidator goUp() {
      return previous;
    }
  }

  private final class AggregateValidator implements TestValidator {

    private final AggregateExpression aggregateExpression;
    private final TestValidator previous;

    protected AggregateValidator(final AggregateExpression aggregateExpression, final TestValidator previous) {
      this.aggregateExpression = aggregateExpression;
      this.previous = previous;
    }

    public AggregateValidator isStandardMethod(final AggregateExpression.StandardMethod method) {
      assertNotNull(aggregateExpression);
      assertEquals(method, aggregateExpression.getStandardMethod());
      return this;
    }

    public AggregateValidator isCustomMethod(final FullQualifiedName method) {
      assertNotNull(aggregateExpression);
      assertEquals(method, aggregateExpression.getCustomMethod());
      return this;
    }

    public AggregateValidator isAlias(final String alias) {
      assertNotNull(aggregateExpression);
      assertEquals(alias, aggregateExpression.getAlias());
      return this;
    }

    public AggregateValidator noExpression() {
        assertNotNull(aggregateExpression);
        assertNull(aggregateExpression.getExpression());

        return this;
      }

    public FilterValidator goExpression() {
      assertNotNull(aggregateExpression);
      assertNotNull(aggregateExpression.getExpression());
      return new FilterValidator().setValidator(this).setEdm(edm)
          .setExpression(aggregateExpression.getExpression());
    }

    public ResourceValidator goPath() {
      assertNotNull(aggregateExpression);
      assertFalse(aggregateExpression.getPath().isEmpty());
      UriInfoImpl resource = new UriInfoImpl().setKind(UriInfoKind.resource);
      for (final UriResource segment : aggregateExpression.getPath()) {
        resource.addResourcePart(segment);
      }
      return new ResourceValidator().setUpValidator(this).setEdm(edm).setUriInfoPath(resource);
    }

    public AggregateValidator noInlineAggregateExpression() {
    	assertNull(aggregateExpression.getInlineAggregateExpression());

    	return this;
    }

    public AggregateValidator goInlineAggregateExpression() {
      return new AggregateValidator(aggregateExpression.getInlineAggregateExpression(), this);
    }

    public AggregateValidator goFrom(final int index) {
      assertTrue(index < aggregateExpression.getFrom().size());
      return new AggregateValidator(aggregateExpression.getFrom().get(index), this);
    }

    public AggregateValidator goUpAggregate() {
      return (AggregateValidator) previous;
    }

    public ApplyValidator goUp() {
      return (ApplyValidator) previous;
    }
  }

  private final class BottomTopValidator implements TestValidator {

    private final BottomTop item;
    private final ApplyValidator previous;

    private BottomTopValidator(final BottomTop item, final ApplyValidator previous) {
      this.item = item;
      this.previous = previous;
    }

    public BottomTopValidator isMethod(final BottomTop.Method method) {
      assertEquals(method, item.getMethod());
      return this;
    }

    public FilterValidator goNumber() {
      assertNotNull(item.getNumber());
      return new FilterValidator().setValidator(this).setEdm(edm).setExpression(item.getNumber());
    }

    public FilterValidator goValue() {
      assertNotNull(item.getValue());
      return new FilterValidator().setValidator(this).setEdm(edm).setExpression(item.getValue());
    }

    public ApplyValidator goUp() {
      return previous;
    }
  }
  
  private final class TopValidator implements TestValidator {

	    private final Top item;
	    private final ApplyValidator previous;

	    private TopValidator(final Top item, final ApplyValidator previous) {
	      this.item = item;
	      this.previous = previous;
	    }

	    public TopValidator goValue(int value) {
	      assertNotNull(item.getTopOption().getValue());
	      assertEquals(value, item.getTopOption().getValue());
	      return this;
	    }

	    public ApplyValidator goUp() {
	      return previous;
	    }
	  }
  
  private final class SkipValidator implements TestValidator {

	    private final Skip item;
	    private final ApplyValidator previous;

	    private SkipValidator(final Skip item, final ApplyValidator previous) {
	      this.item = item;
	      this.previous = previous;
	    }

	    public SkipValidator goValue(int value) {
	      assertNotNull(item.getSkipOption().getValue());
	      assertEquals(value, item.getSkipOption().getValue());
	      return this;
	    }

	    public ApplyValidator goUp() {
	      return previous;
	    }
	  }
  
  private final class OrderByValidator implements TestValidator {

	    private final OrderBy item;
	    private final ApplyValidator previous;

	    private OrderByValidator(final OrderBy item, final ApplyValidator previous) {
	      this.item = item;
	      this.previous = previous;
	    }

	    public OrderByValidator goValue(String expectedOrderBy) throws Exception {
	      List<OrderByItem> orderByItems = item.getOrderByOption().getOrders();
	      List<String> orderBy = new ArrayList<>();
	      for (OrderByItem orderByItem : orderByItems) {
	    	  orderBy.add(String.valueOf(orderByItem.isDescending()) + 
	      FilterTreeToText.Serialize(orderByItem.getExpression()));
	      }
	      assertEquals(expectedOrderBy, "<" + String.join(",", orderBy) + ">");
	      return this;
	    }

	    public ApplyValidator goUp() {
	      return previous;
	    }
	  }

  private final class ComputeValidator implements TestValidator {

    private final ComputeExpression item;
    private final ApplyValidator previous;

    private ComputeValidator(final ComputeExpression item, final ApplyValidator previous) {
      this.item = item;
      this.previous = previous;
    }

    public ComputeValidator isAlias(final String alias) {
      assertEquals(alias, item.getAlias());
      return this;
    }

    public FilterValidator goExpression() {
      assertNotNull(item.getExpression());
      return new FilterValidator().setValidator(this).setEdm(edm).setExpression(item.getExpression());
    }

    public ApplyValidator goUp() {
      return previous;
    }
  }

  private final class GroupByValidator implements TestValidator {

    private final GroupByItem item;
    private final TestValidator previous;

    private GroupByValidator(final GroupByItem item, final TestValidator previous) {
      this.item = item;
      this.previous = previous;
    }

    public ResourceValidator goPath() {
      assertFalse(item.getPath().isEmpty());
      UriInfoImpl resource = new UriInfoImpl().setKind(UriInfoKind.resource);
      for (final UriResource segment : item.getPath()) {
        resource.addResourcePart(segment);
      }
      return new ResourceValidator().setUpValidator(this).setEdm(edm).setUriInfoPath(resource);
    }

    public GroupByValidator isRollupAll() {
      assertTrue(item.isRollupAll());
      return this;
    }

    public GroupByValidator goRollup(final int index) {
      assertTrue(index < item.getRollup().size());
      return new GroupByValidator(item.getRollup().get(index), this);
    }

    public ApplyValidator goUp() {
      return (ApplyValidator) previous;
    }
  }
}
