/*
 * RiskScape™ Copyright New Zealand Institute for Earth Science Limited
 * (Earth Sciences New Zealand) is distributed for research purposes only
 * under the terms of AGPLv3.
 *
 * RiskScape™ Copyright 2026 New Zealand Institute for Earth Science
 * Limited (Earth Sciences New Zealand). All rights reserved. Source code
 * available under the AGPLv3.
 * 
 * This program is free software: you can redistribute it and/or modify it under
 *  the terms of the GNU Affero General Public License as published by the Free
 *  Software Foundation, either version 3 of the License, or (at your option) any
 *  later version.
 * 
 * This program is distributed for RESEARCH PURPOSES ONLY, in the hope that it will
 * be useful for research and education initiatives.
 * 
 * If you are not a researcher, or you are a researcher who wishes to use this
 * program on terms other than AGPLv3 (including those who wish to restrict the
 * distribution of any source code created using this program), please contact:
 * https://riskscape.org.nz
 * 
 * This program is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Affero General Public License for more details.  You should have received a copy
 * of the GNU Affero General Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * By way of summary only, under the AGPLv3:
 *     • Permissions of this strongest copyleft license are conditioned
 *       on making available complete source code of licensed works and
 *       modifications, which include larger works using a licensed work,
 *       under the same license.
 *     • Copyright and license notices must be preserved.
 *     • Contributors provide an express grant of patent rights.
 *     • When a modified version is used to provide a service over a
 *       network, the complete source code of the modified version must be made
 *       available.
 */
package nz.org.riskscape.rl;

import static nz.org.riskscape.engine.Assert.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeDiagnosingMatcher;
import org.junit.Test;

import com.google.common.collect.Lists;

import nz.org.riskscape.dsl.Lexer;
import nz.org.riskscape.dsl.ParseException;
import nz.org.riskscape.dsl.Source;
import nz.org.riskscape.dsl.Token;
import nz.org.riskscape.dsl.TokenMatcher;
import nz.org.riskscape.dsl.TokenType;
import nz.org.riskscape.dsl.UnexpectedTokenException;
import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.rl.ast.BinaryOperation;
import nz.org.riskscape.rl.ast.BracketedExpression;
import nz.org.riskscape.rl.ast.Constant;
import nz.org.riskscape.rl.ast.Expression;
import nz.org.riskscape.rl.ast.ExpressionProblems;
import nz.org.riskscape.rl.ast.FunctionCall;
import nz.org.riskscape.rl.ast.Lambda;
import nz.org.riskscape.rl.ast.ListDeclaration;
import nz.org.riskscape.rl.ast.ParameterToken;
import nz.org.riskscape.rl.ast.PropertyAccess;
import nz.org.riskscape.rl.ast.SelectAllExpression;
import nz.org.riskscape.rl.ast.StructDeclaration;

public class ExpressionParserTest {

  ExpressionParser parser = new ExpressionParser();
  Expression lastParsed;

  @Test
  public void sanityCheckLexer() throws Exception {
    Lexer<TokenTypes> lexer = new Lexer<>(TokenTypes.tokens(), "foo && bar");
    List<TokenTypes> types = Lists.newArrayList();
    while (!lexer.isEOF()) {
      types.add((TokenTypes) lexer.next().type);
    }
    assertEquals(Arrays.asList(TokenTypes.IDENTIFIER, TokenTypes.AND, TokenTypes.IDENTIFIER), types);
  }

  @Test
  public void sanityCheckLexerIdentifiers() throws Exception {
    assertThat(
        lex("foo bar"),
        contains(
            token(TokenTypes.IDENTIFIER, equalTo("foo")),
            token(TokenTypes.IDENTIFIER, equalTo("bar"))
        )
    );

    assertThat(
        lex("foo-bar"),
        contains(
            token(TokenTypes.IDENTIFIER, equalTo("foo")),
            token(TokenTypes.MINUS),
            token(TokenTypes.IDENTIFIER, equalTo("bar"))
        )
    );

    assertThat(
        lex("foo-bar:"),
        contains(
            token(TokenTypes.KEY_IDENTIFIER, equalTo("foo-bar")),
            token(TokenTypes.COLON)
        )
    );

    assertThat(
        lex("foo: bar"),
        contains(
            token(TokenTypes.KEY_IDENTIFIER, equalTo("foo")),
            token(TokenTypes.COLON),
            token(TokenTypes.IDENTIFIER, equalTo("bar"))
        )
    );

    assertThat(
        lex("\"foo baz \"   : bar"),
        contains(
            token(TokenTypes.QUOTED_IDENTIFIER, equalTo("foo baz ")),
            token(TokenTypes.COLON),
            token(TokenTypes.IDENTIFIER, equalTo("bar"))
        )
    );


    assertThat(
        lex("foo-bar    :"),
        contains(
            token(TokenTypes.KEY_IDENTIFIER, equalTo("foo-bar")),
            token(TokenTypes.COLON)
        )
    );


  }

  private Matcher<Token> token(TokenTypes tokenType) {
    return token(tokenType, any(String.class));
  }

  private Matcher<Token> token(TokenTypes tokenType, Matcher<String> valueMatcher) {
    return new TypeSafeDiagnosingMatcher<Token>(Token.class) {

      @Override
      public void describeTo(Description description) {
        description.appendText("token ").appendValue(tokenType.name())
          .appendText(" with value ").appendDescriptionOf(valueMatcher);
      }

      @Override
      protected boolean matchesSafely(Token item, Description mismatchDescription) {
        if (item.type == tokenType) {
          if (valueMatcher.matches(item.getValue())) {
            return true;
          } else {
            mismatchDescription.appendText("value ").appendValue(item.getValue())
              .appendText(" did not match ").appendDescriptionOf(valueMatcher);
            return false;
          }
        } else {
          mismatchDescription.appendText("expected type ").appendValue(tokenType)
            .appendText(" but was ").appendValue(item.type);
        }
        return false;
      }
    };
  }

  private List<Token> lex(String source) {
    Lexer<TokenTypes> lexer = new Lexer<>(TokenTypes.tokens(), source);
    List<Token> tokens = Lists.newArrayList();
    while (!lexer.isEOF()) {
      tokens.add(lexer.next());
    }

    return tokens;
  }

  @Test
  public void emptyExpressionsThrowParseException() throws Exception {
    Problem p = ExpressionProblems.get().emptyStringNotValid();
    assertThat(Assert.assertThrows(ParseException.class, () -> parser.parse("")).getProblem(), equalTo(p));
    assertThat(Assert.assertThrows(ParseException.class, () -> parser.parse(" ")).getProblem(), equalTo(p));
    assertThat(Assert.assertThrows(ParseException.class, () -> parser.parse("\t")).getProblem(), equalTo(p));
    assertThat(Assert.assertThrows(ParseException.class, () -> parser.parse("\r")).getProblem(), equalTo(p));
    assertThat(Assert.assertThrows(ParseException.class, () -> parser.parse("\n")).getProblem(), equalTo(p));
  }

  @Test
  public void testSomeLiteralExpressions() throws Exception {
    expectType(Constant.class, "1");
    expectType(Constant.class, "-1");
    expectType(Constant.class, "-0");
    expectType(Constant.class, "0");
    expectType(Constant.class, "'hi there'");
    expectType(Constant.class, "true");
    expectType(Constant.class, "FALSE");
    expectType(Constant.class, "nUll");
    expectType(Constant.class, "10E9");
    expectType(Constant.class, "10E-9");
  }

  @Test
  public void testSomeEscapingInStrings() throws Exception {
    TokenMatcher matcher = TokenMatcher.forQuotedString('\'');
    TokenType t = TokenTypes.STRING;

    BiConsumer<String, String> tester = (source, expected) -> {
      Token matched = matcher.match(t, Source.anon(source), 0);
      assertNotNull(matched);
      assertEquals(expected, matched.value);
    };

    tester.accept("'this \\ backspace is wanted'", "this \\ backspace is wanted");
    tester.accept("'these \\'backspaces\\' are escaped'", "these 'backspaces' are escaped");
    tester.accept("'these are \\\\\\' wanted'", "these are \\' wanted");
  }

  @Test
  public void testSomeBinaryExpressions() throws Exception {
    expectType(BinaryOperation.class, "1 + 1");
    expectType(BinaryOperation.class, "-1 - 5");
    expectType(BinaryOperation.class, "26 * 4");
    expectType(BinaryOperation.class, "26 ** 4");
    expectType(BinaryOperation.class, "0 / 4");
    expectType(BinaryOperation.class, "0 || 4");
  }

  @Test
  public void testPrioritizedMathsExpressions() throws Exception {
    expectType(BracketedExpression.class, expectType(BinaryOperation.class, "(1 + 1) * 2").getLhs());
    expectType(BracketedExpression.class, expectType(BinaryOperation.class, "1 + (1 * 2)").getRhs());
  }

  @Test
  public void testAFunctionExpression() throws Exception {

    FunctionCall fc = expectType(FunctionCall.class, "foo(bar, baz.foo)");
    assertEquals("foo", fc.getIdentifier().value);

    PropertyAccess first = expectType(PropertyAccess.class, fc.getArguments().get(0).getExpression());
    PropertyAccess second = expectType(PropertyAccess.class, fc.getArguments().get(1).getExpression());

    assertEquals("bar", first.getIdentifiers().get(0).value);
    assertEquals("baz", second.getIdentifiers().get(0).value);
    assertEquals("foo", second.getIdentifiers().get(1).value);
  }

  @Test
  public void testIdentifiers() throws Exception {
    assertEquals("a", expectType(PropertyAccess.class, "a").getFirstIdentifier().value);
    assertEquals("a", expectType(PropertyAccess.class, "a").getLastIdentifier().value);
    assertEquals("a0", expectType(PropertyAccess.class, "a0").getIdentifiers().get(0).value);

    PropertyAccess pa = expectType(PropertyAccess.class, "a.b.c");
    assertEquals(pa.getFirstIdentifier().value, "a");
    assertEquals(pa.getLastIdentifier().value, "c");
  }

  @Test
  public void testBadIdentifiers() throws Exception {
    Assert.assertThrows(MalformedExpressionException.class, () ->
      expectType(PropertyAccess.class, "0a"));

    //Identifiers with : in need to be quoted
    Assert.assertThrows(MalformedExpressionException.class, () ->
      expectType(PropertyAccess.class, "a:"));

    //Identifiers with spaces need to be quoted
    Assert.assertThrows(MalformedExpressionException.class, () ->
      expectType(PropertyAccess.class, "a b"));

    Assert.assertThrows(MalformedExpressionException.class, () ->
      expectType(PropertyAccess.class, "a:"));
  }

  @Test
  public void testMalformedScientificNumbers() {
    Assert.assertThrows(MalformedExpressionException.class, () -> expectType(Constant.class, "1 e12"));
    Assert.assertThrows(MalformedExpressionException.class, () -> expectType(Constant.class, "1..3e5"));
    Assert.assertThrows(MalformedExpressionException.class, () -> expectType(Constant.class, "1e 12"));
    Assert.assertThrows(MalformedExpressionException.class, () -> expectType(Constant.class, "1.5e1.5"));
    Assert.assertThrows(MalformedExpressionException.class, () -> expectType(Constant.class, ".05e12"));
    Assert.assertThrows(MalformedExpressionException.class, () -> expectType(Constant.class, "1Ee3"));

    // leading plus is not allowed
    Assert.assertThrows(MalformedExpressionException.class, () -> expectType(Constant.class, "+1.2e5"));
  }


  @Test
  public void testQuotedIdentifiers() throws Exception {
    assertEquals("foo:bar", expectType(PropertyAccess.class, "\"foo:bar\"").getIdentifiers().get(0).value);
    assertEquals("a:0", expectType(PropertyAccess.class, "\"a:0\"").getIdentifiers().get(0).value);

    PropertyAccess access = expectType(PropertyAccess.class, "\"foo is foo\"");
    assertEquals("foo is foo", access.getIdentifiers().get(0).value);

    access = expectType(PropertyAccess.class, "\"foo is foo\".\"is this \\\"a bar?\"");
    assertEquals("foo is foo", access.getIdentifiers().get(0).value);
    assertEquals("is this \"a bar?", access.getIdentifiers().get(1).value);

  }

  @Test
  public void testFunctionCallWithQuotedIdentifier() throws Exception {
    FunctionCall fc = expectType(FunctionCall.class, "\"foo:bar\"(foo, bar)");

    assertThat(fc.getIdentifier().type, is(TokenTypes.QUOTED_IDENTIFIER));
    assertThat(fc.getIdentifier().getValue(), is("foo:bar"));

    assertThat(fc.toSource(), is("\"foo:bar\"(foo, bar)"));
  }

  @Test
  public void testNestedFunctionExpressions() throws Exception {
    FunctionCall fc;

    fc = expectType(FunctionCall.class, "foo(bar, baz(foo()))");
    assertEquals("foo", fc.getIdentifier().value);

    PropertyAccess first = expectType(PropertyAccess.class, fc.getArguments().get(0).getExpression());
    assertEquals("bar", first.getIdentifiers().get(0).value);

    fc = expectType(FunctionCall.class, fc.getArguments().get(1).getExpression());
    assertEquals("baz", fc.getIdentifier().value);

    fc = expectType(FunctionCall.class, fc.getArguments().get(0).getExpression());
    assertEquals("foo", fc.getIdentifier().value);
    assertEquals(0, fc.getArguments().size());
  }

  @Test
  public void testIndexingTheResultOfAFunction() throws Exception {
    PropertyAccess access = expectType(PropertyAccess.class, "foo(bar).baz");
    expectType(FunctionCall.class, access.getReceiver().get());
    assertEquals("baz", access.getIdentifiers().get(0).value);
  }

  @Test
  public void canDoBinaryOperationsAsFunctionArgs() throws Exception {
    FunctionCall fc = expectType(FunctionCall.class, "foo(bar / 3, 1.0 * 6)");
    assertEquals(BinaryOperation.class, fc.getArguments().get(0).getExpression().getClass());
    assertEquals(BinaryOperation.class, fc.getArguments().get(1).getExpression().getClass());
  }

  @Test
  public void unexpectedTokensAtTheEndOfAnExpressionAreAProblem() throws Exception {
    assertEquals(TokenTypes.IDENTIFIER,
        Assert.assertThrows(MalformedExpressionException.class, () -> parser.parse("foo bar")).getGot().type);
  }

  @Test
  public void unexpectedTokensInExpressionAreAProblem() throws Exception {
    MalformedExpressionException ex = Assert.assertThrows(MalformedExpressionException.class,
        () -> parser.parse("foo(-))"));
    assertEquals(TokenTypes.MINUS, ex.getGot().type);
  }


  @Test
  public void unexpectedTokensAtBeginningAreAProblem() throws Exception {
    Assert.assertThrows(MalformedExpressionException.class, () -> parser.parse("= foo"));
  }

  @Test
  public void unexpectedTokensGiveTheUserAGenericTipForHowToFix() throws Exception {
    MalformedExpressionException ex =
        Assert.assertThrows(MalformedExpressionException.class, () -> parser.parse("= foo"));

    assertThat(
        ex.getProblem(),
        Matchers.hasAncestorProblem(equalTo(ExpressionProblems.get().malformedExpressionTip()))
    );
  }

  @Test
  public void canParseAListDeclaration() throws Exception {
    ListDeclaration list1 = expectType(ListDeclaration.class, "[1, 2, 3]");
    for (Expression child : list1.getElements()) {
      assertTrue(child instanceof Constant);
    }

    ListDeclaration list2 = expectType(ListDeclaration.class, "[foo, 2, baz([])]");
    assertTrue(list2.getElements().get(0) instanceof PropertyAccess);
    assertTrue(list2.getElements().get(1) instanceof Constant);
    assertTrue(list2.getElements().get(2) instanceof FunctionCall);
  }

  @Test
  public void canParseAListDeclarationWithTrailingComma() {
    ListDeclaration list1 = (ListDeclaration)parser.parse("[1, 2, 3, ]");
    // the trailling comma isn't in the source, we can live with that
    assertThat(list1.toSource(), is("[1, 2, 3]"));
    // check for the expected three constant expression elements
    assertThat(list1.getElements(), contains(
        isA(Constant.class),
        isA(Constant.class),
        isA(Constant.class)
    ));
  }

  @Test
  public void failsWhenListMissingCommaSeparators() {
    MalformedExpressionException ex = assertThrows(MalformedExpressionException.class, () -> parser.parse("[ 3 4]"));
    assertThat(ex.getExpected(), contains(TokenTypes.COMMA));
    assertThat(ex.getGot().getValue(), is("4"));
  }

  @Test
  public void canParseAStructDeclaration() throws Exception {
    StructDeclaration struct1 = expectType(StructDeclaration.class, "{asdf: foo}");
    StructDeclaration.Member entry = struct1.getMembers().get(0);

    assertEquals(entry.getName().get(), "asdf");
    assertTrue(entry.isJSONStyle());
    assertFalse(entry.isSQLStyle());
    assertFalse(entry.isAnonymous());
    assertEquals(entry.getExpression().toSource(), "foo");

    // toRCQL comparison will check it's been read properly
    expectType(StructDeclaration.class, "{asdf: foo, \"awkward fu\": bar}");
    expectType(FunctionCall.class, "my_function({asdf: foo, \"awkward fu\": bar})");
  }

  @Test
  public void canParseAStructDeclarationWithTraillingComma() {
    StructDeclaration struct1 = (StructDeclaration)parser.parse("{asdf: foo,}");
    // the trailling comma isn't in the source, we can live with that
    assertThat(struct1.toSource(), is("{asdf: foo}"));
    assertThat(struct1.getMembers(), hasSize(1));
    StructDeclaration.Member entry = struct1.getMembers().get(0);
    assertEquals(entry.getName().get(), "asdf");
    assertEquals(entry.getExpression().toSource(), "foo");
  }

  @Test
  public void canParseAStructDeclaration_AsSyntax() throws Exception {
    StructDeclaration struct1 = expectType(StructDeclaration.class, "{foo as asdf}");
    StructDeclaration.Member entry = struct1.getMembers().get(0);

    assertEquals(entry.getName().get(), "asdf");
    assertEquals(entry.getExpression().toSource(), "foo");
    assertFalse(entry.isJSONStyle());
    assertTrue(entry.isSQLStyle());
    assertFalse(entry.isAnonymous());

    // toRCQL comparison will check it's been read properly
    expectType(StructDeclaration.class, "{foo as asdf, bar as \"awkward fu\"}");
    expectType(FunctionCall.class, "my_function({foo as asdf, bar as \"awkward fu\"})");
  }

  @Test
  public void canParseAStructDeclaration_AsSyntax_InferAttrNames() throws Exception {
    StructDeclaration struct1 = expectType(StructDeclaration.class, "{foo}");
    StructDeclaration.Member entry = struct1.getMembers().get(0);

    assertFalse(entry.getName().isPresent());
    assertEquals(entry.getExpression().toSource(), "foo");
    assertFalse(entry.isJSONStyle());
    assertFalse(entry.isSQLStyle());
    assertTrue(entry.isAnonymous());

    // toRCQL comparison will check it's been read properly
    expectType(StructDeclaration.class, "{foo, bar}");
    expectType(FunctionCall.class, "my_function({foo, bar as \"awkward fu\"})");
  }

  @Test
  public void failsWhenStructMissingCommaSeparators() {
    MalformedExpressionException ex = assertThrows(MalformedExpressionException.class,
        () -> parser.parse("{foo: 'bar' baz: 'baz'}"));
    assertThat(ex.getExpected(), contains(TokenTypes.COMMA));
    assertThat(ex.getGot().getValue(), is("baz"));

    ex = assertThrows(MalformedExpressionException.class,
        () -> parser.parse("{'bar' 'baz'}"));
    assertThat(ex.getExpected(), contains(TokenTypes.COMMA));
    assertThat(ex.getGot().getValue(), is("baz"));

    ex = assertThrows(MalformedExpressionException.class,
        () -> parser.parse("{'foo' as foo 'bar' as bar1}"));
    assertThat(ex.getExpected(), contains(TokenTypes.COMMA));
    assertThat(ex.getGot().getValue(), is("bar"));
  }

  @Test
  public void failsParseStructDeclaration_MixedSyntax() {
    assertThrows(MalformedExpressionException.class, () -> parser.parse("{foo: 'bar', 19 as bar}"));
    assertThrows(MalformedExpressionException.class, () -> parser.parse("{19 as bar, foo: 'bar'}"));
  }

  @Test
  public void canParseAStructDeclaration_justASelectAll() throws Exception {
    expectType(StructDeclaration.class, "{*}");
  }

  @Test
  public void canParseAStructDeclaration_AsSyntax_leadingSelectAll() throws Exception {
    expectType(StructDeclaration.class, "{*, 'foo' as bar}");
  }

  @Test
  public void canParseAStructDeclaration_AsSyntax_trailingSelectAll() throws Exception {
    expectType(StructDeclaration.class, "{'foo' as bar, *}");
  }

  @Test
  public void canParseAStructDeclaration_AsSyntax_squashedSelectAll() throws Exception {
    expectType(StructDeclaration.class, "{'foo' as bar, *, 'baz' as foo}");
  }


  @Test
  public void canParseAStructDeclaration_ColonSyntax_leadingSelectAll() throws Exception {
    expectType(StructDeclaration.class, "{*, foo: bar}");
  }

  @Test
  public void canParseAStructDeclaration_ColonSyntax_trailingSelectAll() throws Exception {
    expectType(StructDeclaration.class, "{foo: bar, *}");
  }

  @Test
  public void canParseAStructDeclaration_ColonSyntax_squashedSelectAll() throws Exception {
    expectType(StructDeclaration.class, "{foo: bar, *, baz: foo}");
  }

  @Test
  public void failsParseStructDeclaration_selectAllCanNotBeRepeated() throws Exception {
    assertThrows(MalformedExpressionException.class, () -> parser.parse("{*, *}"));
    assertThrows(MalformedExpressionException.class, () -> parser.parse("{*, foo, *}"));
    assertThrows(MalformedExpressionException.class, () -> parser.parse("{*, foo, *, bar}"));
    MalformedExpressionException ex =
        assertThrows(MalformedExpressionException.class, () -> parser.parse("{foo, *, *, bar}"));

    assertThat(
      ex.getProblem(),
      allOf(
        // check for the custom message
        Matchers.hasAncestorProblem(Matchers.equalIgnoringChildren(
          ExpressionProblems.get().duplicateSelectAll(Token.token(TokenTypes.MULTIPLY, "*"))
        )),

          // no tip should be included - the custom message should be there instead
        not(Matchers.hasAncestorProblem(equalTo(ExpressionProblems.get().malformedExpressionTip())))
      )
    );
  }

  @Test
  public void aSelectAllIsAValidExpression() throws Exception {
    expectType(SelectAllExpression.class, "*");
  }

  @Test
  public void canSpecifySelectAllAsAStructMember() throws Exception {
    StructDeclaration decl = expectType(StructDeclaration.class, "{foo: *, bar: baz}");
    assertEquals(2, decl.getMembers().size());
    assertEquals("foo", decl.getMembers().get(0).getAttributeName());
    assertThat(decl.getMembers().get(0).getExpression(), instanceOf(SelectAllExpression.class));

    assertEquals("bar", decl.getMembers().get(1).getAttributeName());
    assertThat(decl.getMembers().get(1).getExpression(), instanceOf(PropertyAccess.class));
  }

  @Test
  public void canSelectAllAllMembersFromAStruct() throws Exception {
    StructDeclaration decl = expectType(StructDeclaration.class, "{foo.*, bar as baz}");
    assertEquals(2, decl.getMembers().size());

    assertThat(decl.getMembers().get(0).getExpression(), instanceOf(PropertyAccess.class));
    PropertyAccess access = (PropertyAccess) decl.getMembers().get(0).getExpression();
    assertEquals("foo", access.getFirstIdentifier().getValue());
    assertTrue(access.isTrailingSelectAll());
  }

  // this test isn't really testing a feature - it's more here to clarify the behaviour/limitation of the parser
  // wrt to splatting a receiver and colon syntax
  @Test(expected = MalformedExpressionException.class)
  public void canNotSelectAllAllMembersFromAStructWithColonSyntax() throws Exception {
    parser.parse("{foo.*, baz: 1}");
  }

  // this test isn't really testing a feature - it's more here to clarify the behaviour/limitation of the parser
  // wrt to splatting a receiver and colon syntax
  @Test(expected = MalformedExpressionException.class)
  public void canNotSelectAllAllMembersFromAStructWithColonSyntaxOtherWayAround() throws Exception {
    parser.parse("{baz: 1, foo.*}");
  }

  @Test
  public void testPropertyAccessCanEndInSelectAll() throws Exception {
    PropertyAccess property = expectType(PropertyAccess.class, "foo.*");
    assertTrue(property.isTrailingSelectAll());
    assertEquals("foo", property.getFirstIdentifier().getValue());
  }

  @Test
  public void testFunctionPropertyAccessCanEndInSelectAll() throws Exception {
    PropertyAccess property = expectType(PropertyAccess.class, "foo().bar.*");
    assertEquals("bar", property.getFirstIdentifier().getValue());
    assertTrue(property.isTrailingSelectAll());
    assertFalse(property.isReceiverSelectAll());
  }

  @Test
  public void testFunctionCallCanEndInSelectAll() throws Exception {
    PropertyAccess property = expectType(PropertyAccess.class, "foo().*");
    assertEquals("*", property.getFirstIdentifier().getValue());
    assertTrue(property.isTrailingSelectAll());
    assertTrue(property.isReceiverSelectAll());
  }

  @Test
  public void hyphensAreAllowedInStructKeyIdentifiers() throws Exception {
    // toSource puts spaces between operands of a binary operations
    StructDeclaration structDecl = expectType(StructDeclaration.class, "{foo-bar: baz-foo}", "{foo-bar: baz - foo}");
    assertEquals(1, structDecl.getMembers().size());
    assertEquals("foo-bar", structDecl.getMembers().get(0).getAttributeName());

    BinaryOperation bop = (BinaryOperation) structDecl.getMembers().get(0).getExpression();
    assertEquals("baz", bop.getLhs().toSource());
    assertEquals("-", bop.getOperator().value);
    assertEquals("foo", bop.getRhs().toSource());
  }

  @Test
  public void asSyntaxAllowsFunctionCalls() throws Exception {
    // toSource puts spaces between operands of a binary operations
    StructDeclaration structDecl = expectType(StructDeclaration.class, "{sum(foo) as sum}");
    assertEquals(1, structDecl.getMembers().size());
    StructDeclaration.Member attr = structDecl.getMembers().get(0);
    FunctionCall call = (FunctionCall) attr.getExpression();

    assertEquals(call.toSource(), "sum(foo)");
    assertEquals(call.getIdentifier().getValue(), "sum");
  }

  @Test
  public void hyphensAreAllowedInFunctionArgumentKeyIdentifiers() throws Exception {
    // toSource puts spaces between operands of a binary operations
    FunctionCall functionDecl = expectType(FunctionCall.class, "foo(foo-bar: baz-foo)", "foo(foo-bar: baz - foo)");
    assertEquals("foo", functionDecl.getIdentifier().getValue());
    assertEquals(1, functionDecl.getArguments().size());
    assertEquals(Optional.of("foo-bar"), functionDecl.getArguments().get(0).getName());

    BinaryOperation bop = (BinaryOperation) functionDecl.getArguments().get(0).getExpression();
    assertEquals("baz", bop.getLhs().toSource());
    assertEquals("-", bop.getOperator().value);
    assertEquals("foo", bop.getRhs().toSource());
  }

  @Test
  public void canParseLambdaExpression() throws Exception {
    Lambda lambda = expectType(Lambda.class, "(foo) -> foo.bar");
    assertEquals("foo", lambda.getArguments().get(0).getValue());
    assertEquals("foo.bar", lambda.getExpression().toSource());
  }

  @Test
  public void canParseLambdaExpressionWithoutArgBrackets() throws Exception {
    Lambda lambda = expectType(Lambda.class, "foo -> foo.bar");
    assertEquals("foo", lambda.getArguments().get(0).getValue());
    assertEquals("foo.bar", lambda.getExpression().toSource());
  }

  @Test
  public void canParseManyArgLambdaExpression() throws Exception {
    Lambda lambda = expectType(Lambda.class, "(foo, bar) -> foo && bar");
    assertEquals("foo", lambda.getArguments().get(0).getValue());
    assertEquals("bar", lambda.getArguments().get(1).getValue());
    assertEquals("foo && bar", lambda.getExpression().toSource());
  }

  @Test
  public void canParseZeroArgLambdaExpression() throws Exception {
    Lambda lambda = expectType(Lambda.class, "() -> foo");
    assertTrue(lambda.getArguments().isEmpty());
    assertEquals("foo", lambda.getExpression().toSource());
  }

  @Test
  public void canParseLambdaExpressionAsFunctionArgument() throws Exception {
    FunctionCall call = expectType(FunctionCall.class, "map(foo -> foo.bar)");
    Lambda lambda = (Lambda) call.getArguments().get(0).getExpression();
    assertEquals("foo", lambda.getArguments().get(0).getValue());
    assertEquals("foo.bar", lambda.getExpression().toSource());
  }

  @Test
  public void canTurnAnyExpressionInToAStructDeclaration() throws Exception {
    assertEquals("{foo}", parser.toStruct(expectType(PropertyAccess.class, "foo")).toSource());
    assertEquals("{foo()}", parser.toStruct(expectType(FunctionCall.class, "foo()")).toSource());
    assertEquals("{}", parser.toStruct(expectType(StructDeclaration.class, "{}")).toSource());
  }

  @Test
  public void canTurnAnyExpressionInToAListDeclaration() throws Exception {
    assertEquals("[foo]", parser.toList(expectType(PropertyAccess.class, "foo")).toSource());
    assertEquals("[foo()]", parser.toList(expectType(FunctionCall.class, "foo()")).toSource());
    assertEquals("[]", parser.toList(expectType(ListDeclaration.class, "[]")).toSource());
    assertEquals("[foo, bar]", parser.toList(expectType(ListDeclaration.class, "[foo, bar]")).toSource());
  }

  @Test
  public void parameterTokensAreAllowedPrettyMuchAnywhere() throws Exception {
    // TODO elaborate these
    assertEquals("foo", parser.parseAllowParameters("$foo").isA(ParameterToken.class).get().getValue());
    assertEquals("bar", parser.parseAllowParameters("foo($bar)").isA(FunctionCall.class).get().getArguments()
        .get(0).getExpression().isA(ParameterToken.class).get().getValue());
  }

  @Test
  public void parametersCauseParsingToBlowUp() throws Exception {
    // TODO elaborate these
    assertThrows(UnexpectedTokenException.class, () -> parser.parse("$foo"));
    assertThrows(UnexpectedTokenException.class, () -> parser.parse("foo($bar)"));
  }

  protected <T> T expectType(Class<T> expected, String rcql) {
    return expectType(expected, rcql, rcql);
  }
  protected <T> T expectType(Class<T> expected, String rcql, String match) {
    lastParsed =  parser.parse(rcql);
    assertEquals(match, lastParsed.toSource());
    return expectType(expected, lastParsed);
  }

  protected <T> T expectType(Class<T> expected, Object toCheck) {
    if (expected.isInstance(toCheck)) {
      return expected.cast(toCheck);
    } else {
      fail("Expected " + toCheck + " to be of type " + expected + ", but was " + toCheck.getClass());
      throw new RuntimeException("never happens");
    }
  }

}

