/*
 * 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.engine.rl;

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

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import nz.org.riskscape.dsl.LexerException;
import nz.org.riskscape.dsl.LexerProblems;
import nz.org.riskscape.dsl.ParseException;
import nz.org.riskscape.dsl.SourceLocation;
import nz.org.riskscape.engine.Assert;
import nz.org.riskscape.engine.DummyFunction;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.BaseRealizableFunction;
import nz.org.riskscape.engine.function.BinaryFunction;
import nz.org.riskscape.engine.function.FunctionArgument;
import nz.org.riskscape.engine.function.IdentifiedFunction;
import nz.org.riskscape.engine.function.IdentifiedFunction.Category;
import nz.org.riskscape.engine.function.RiskscapeFunction;
import nz.org.riskscape.engine.function.UnaryFunction;
import nz.org.riskscape.engine.resource.Resource;
import nz.org.riskscape.engine.types.EmptyList;
import nz.org.riskscape.engine.types.LambdaType;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.Struct;
import nz.org.riskscape.engine.types.Type;
import nz.org.riskscape.engine.types.TypeProblems;
import nz.org.riskscape.engine.types.Types;
import nz.org.riskscape.engine.types.WithinSet;
import nz.org.riskscape.problem.Problem;
import nz.org.riskscape.problem.Problem.Severity;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.ExpressionProblems;
import nz.org.riskscape.rl.ast.FunctionCall;
import nz.org.riskscape.rl.ast.PropertyAccess;
import nz.org.riskscape.rl.ast.StructDeclaration;

@SuppressWarnings("unchecked")
public class ExpressionRealizerTest extends BaseExpressionRealizerTest {

  @Before
  public void before() {
    project.getFunctionSet().insertFirst(DefaultOperators.INSTANCE);
    project.getFunctionSet().addAll(MathsFunctions.FUNCTIONS);
    project.getFunctionSet().addAll(LanguageFunctions.FUNCTIONS);
    project.getFunctionSet().addAll(LogicFunctions.LOGIC_FUNCTIONS);
    realizationContext = project.newRealizationContext();
    expressionRealizer = new DefaultExpressionRealizer(realizationContext);
    realizer = expressionRealizer;
  }

  @Test
  public void canRealizeNumbers() {
    // integers
    assertThat(evaluate("5", null), is(5L));
    assertThat(evaluate("05", null), is(5L));
    assertThat(evaluate("-5", null), is(-5L));
    assertThat(evaluate("-05", null), is(-5L));

    // floating
    assertThat(evaluate("1.2", null), is(1.2D));
    assertThat(evaluate("-0.05", null), is(-0.05));

    // scientific numbers
    assertThat(evaluate("1E3", null), is(1000D));
    assertThat(evaluate("1.25E3", null), is(1250D));
    assertThat(evaluate("-1.25E3", null), is(-1250D));
    // with optional + on the exponent
    assertThat(evaluate("1E+3", null), is(1000D));
    assertThat(evaluate("1.25E+3", null), is(1250D));
    assertThat(evaluate("-1.25E+3", null), is(-1250D));

    assertThat(evaluate("1E-3", null), is(0.001D));
    assertThat(evaluate("1.25E-3", null), is(0.00125D));
    assertThat(evaluate("-1.25E-3", null), is(-0.001250));

    // little e is fine as well
    assertThat(evaluate("1e-3", null), is(0.001D));
    assertThat(evaluate("1.25e-3", null), is(0.00125D));
    assertThat(evaluate("-1.25e-3", null), is(-0.001250));
  }

  @Test
  public void canEvaluateExpressionAgainstInputs() throws Exception {
    Struct type = Struct.of("foo", Types.INTEGER, "bar", Types.INTEGER);
    RealizedExpression e = realize(type, parse("foo + bar"));

    // this is the normal path of evaulating an expression
    assertThat(e.evaluate(Tuple.ofValues(type, 2L, 3L)), is(5L));

    // and now we have this little convenience to remove some tuple wrangling.
    assertThat(e.evaluateValues(2L, 3L), is(5L));
  }

  @Test
  public void addPropertiesFromStruct() throws Exception {
    inputStruct = Struct.of("foo", Types.INTEGER, "bar", Types.INTEGER);
    evaluate("foo + bar", Tuple.ofValues((Struct) inputStruct, 3L, 4L));
    assertEquals(Types.INTEGER, realized.getResultType());
  }

  @Test
  public void binaryOperations_HaveBEDMAS_Precedence() throws Exception {
    assertEquals(55L, evaluate("5 + 5 * 10", null));
    assertEquals(55L, evaluate("(5 * 10) + 5", null));
    assertEquals(55L, evaluate("5 * 10 + 5", null));

    assertEquals(-25L, evaluate("5 - 3 * 10", null));
    assertEquals(35L, evaluate("(3 * 10) + 5", null));
    assertEquals(35L, evaluate("3 * 10 + 5", null));

    assertEquals(25.0D, evaluate("(10 / 2) * 5", null));
    assertEquals(25.0D, evaluate("10 / 2 * 5", null));

    assertEquals(135.0D, evaluate("3 ** 3 * 10 / 2", null));
    assertEquals(135.0D, evaluate("(3 ** 3) * (10 / 2)", null));


    assertEquals(16.0D, evaluate("(3 * (10 / 5)) + 10", null));
    assertEquals(16.0D, evaluate("3 * 10 / 5 + 10", null));

    assertEquals(26D, evaluate("2 * 2 ** 3 + 10", null));
    assertEquals(974D, evaluate("1000 - 2 * 2 ** 3 + 10", null));
    assertEquals(948D, evaluate("1000 - 2 * 2 ** 3 + 10 - 2 * 5 - 2 ** 4", null));
    assertEquals(948D, evaluate("1000 - ((2 * (2 ** 3)) + 10) - (2 * 5) - (2 ** 4)", null));

    assertEquals(893.75D, evaluate(
        "1000 - (2 / (2 ** 3)) - (10 + (2 * 5)) - (2 ** 4) - ((((8 / 2) * 3)) + 10) - (2 ** 5) - (2 ** 4)", null));
    assertEquals(893.75D, evaluate(
        "1000 - 2 / 2 ** 3 - 10 + 2 * 5 - 2 ** 4 - 8 / 2 * 3 + 10 - 2 ** 5 - 2 ** 4", null));

    assertEquals(-81687.25D, evaluate(
        "10000 - (((2 ** 3) * (10 / 5)) + 20) - (5 * ((55 ** 2) / 100)) - (300 ** 2) - (1000 + 500)", null));
    assertEquals(-81687.25D, evaluate(
        "10000 - 2 ** 3 * 10 / 5 + 20 - 5 * 55 ** 2 / 100 - 300 ** 2 - 1000 + 500", null));

    assertThat(evaluate("(2 + 3) > 2 + 2", null), is(true));
    assertThat(evaluate("2 + 3 > 2 + 2", null), is(true));

    assertThat(evaluate("(2 < 3) and (4 < 5)", null), is(true));
    assertThat(evaluate("(2 < 3) and 4 < 5", null), is(true));
    assertThat(evaluate("2 < 3 and 4 < 5", null), is(true));

    assertThat(evaluate("(2 < 3) && (4 < 5)", null), is(true));
    assertThat(evaluate("(2 < 3) && 4 < 5", null), is(true));
    assertThat(evaluate("2 < 3 && 4 < 5", null), is(true));
  }


  @Test
  public void binaryOperations_BEDMAS_Left_To_Right_Same_Precedence() throws Exception {
    //BEDMAS does not remove all ambiguity.
    //A chain of division will give different results if you go right-to-left or left-to-right.
    //left-to-right is what we do.
    assertEquals(1.0D, evaluate("10 / 5 / 2", null));
    assertEquals(1.0D, evaluate("(10 / 5) / 2", null));

    //not this right-to-left version
    assertEquals(4.0D, evaluate("10 / (5 / 2)", null));
  }

  @Test
  public void binaryOperations_Errors() throws Exception {
    //Testing the realizing with BEDMAS precedence does not make error messages incomprehensible.
    evaluate("2 + 'cat' > 2 + 2", null);
    assertThat(realizationProblems,
        contains(isError(ExpressionProblems.class, "noSuchOperatorFunction")));
  }

  @Test
  public void doMathsOnSomeConstants() throws Exception {
    assertEquals(3L, evaluate("1 + 2", null));
    assertEquals(Types.INTEGER, realized.getResultType());

    assertEquals(-1L, evaluate("1 - 2", null));
    assertEquals(Types.INTEGER, realized.getResultType());

    assertEquals(1.5D, evaluate("0.25 - -1.25", null));
    assertEquals(Types.FLOATING, realized.getResultType());

    //Integer division results in floating
    assertEquals(1.0D, evaluate("2 / 2", null));
    assertEquals(Types.FLOATING, realized.getResultType());

    assertEquals(1D, evaluate(".2 / .2", null));
    assertEquals(Types.FLOATING, realized.getResultType());
  }

  @Test
  public void doMathsOnSomeMixedTypeConstants() throws Exception {
    assertEquals(3.0D, evaluate("1 + 2.0", null));
    assertEquals(Types.FLOATING, realized.getResultType());

    assertEquals(3.0D, evaluate("1.0 + 2", null));
    assertEquals(Types.FLOATING, realized.getResultType());

    assertEquals(2.0D, evaluate("1.0 * 2", null));
    assertEquals(Types.FLOATING, realized.getResultType());

    assertEquals(1.0D, evaluate("2.0 / 2", null));
    assertEquals(Types.FLOATING, realized.getResultType());
  }

  @Test
  public void doMathsOnPropertiesAndConstants() throws Exception {
    Struct inputType = Struct.of("x", Types.INTEGER, "y", Types.FLOATING);
    assertEquals(4.0D, evaluate("1 + y", Tuple.ofValues(inputType, null, 3.0D)));
    assertEquals(Types.FLOATING, realized.getResultType());

    assertEquals(2.0D, evaluate("10 / x", Tuple.ofValues(inputType, 5L, null)));
    assertEquals(Types.FLOATING, realized.getResultType());

    assertEquals(2.0D, evaluate("1.0 * 2", null));
    assertEquals(Types.FLOATING, realized.getResultType());

    assertEquals(1.0D, evaluate("2.0 / 2", null));
    assertEquals(Types.FLOATING, realized.getResultType());
  }

  @Test
  public void doSomeLogicalMathsStuff() throws Exception {
    assertEquals(Boolean.FALSE, evaluate("(1 + 2) > 4", null));
    assertEquals(Boolean.FALSE, evaluate("1 = 2", null));
    assertEquals(Boolean.TRUE, evaluate("1 <> 2", null));
  }

  @Test
  public void doSomeLogicalMathsStuffWithMixedTypes() throws Exception {
    assertEquals(Boolean.TRUE, evaluate("1 = 1.0", null));

    assertEquals(Boolean.TRUE, evaluate("(1 + 2) < 4.0", null));
    assertEquals(Boolean.FALSE, evaluate("(1 + 2.0) > 4", null));
    assertEquals(Boolean.FALSE, evaluate("1 = 2", null));
    assertEquals(Boolean.FALSE, evaluate("1 = 2.0", null));
    assertEquals(Boolean.TRUE, evaluate("1.0 <= 2", null));
  }

  @Test
  public void testSomeBooleanLogic() throws Exception {
    assertEquals(Boolean.TRUE, evaluate("true <> false", null));
    assertEquals(Boolean.FALSE, evaluate("true = false", null));
    assertEquals(Boolean.TRUE, evaluate("true", null));
    assertEquals(Boolean.TRUE, evaluate("true and true", null));
    assertEquals(Boolean.TRUE, evaluate("true && true", null));
  }

  @Test
  public void testSomeBooleanLogicAndNullableInput() throws Exception {
    Struct input = Struct.of("value", Nullable.BOOLEAN);

    assertEquals(Boolean.TRUE, evaluate("true <> value", Tuple.ofValues(input, false)));
    assertEquals(Boolean.FALSE, evaluate("true = value", Tuple.ofValues(input, false)));
    assertEquals(Boolean.TRUE, evaluate("true and value", Tuple.ofValues(input, true)));
    assertEquals(Boolean.TRUE, evaluate("true && value", Tuple.ofValues(input, true)));

    assertEquals(Boolean.TRUE, evaluate("value <> false", Tuple.ofValues(input, true)));
    assertEquals(Boolean.FALSE, evaluate("value = false", Tuple.ofValues(input, true)));
    assertEquals(Boolean.TRUE, evaluate("value and true", Tuple.ofValues(input, true)));
    assertEquals(Boolean.TRUE, evaluate("value && true", Tuple.ofValues(input, true)));

    //null on the right
    assertEquals(null, evaluate("true <> value", Tuple.ofValues(input)));
    assertEquals(null, evaluate("true = value", Tuple.ofValues(input)));
    assertEquals(null, evaluate("true and value", Tuple.ofValues(input)));
    assertEquals(null, evaluate("true && value", Tuple.ofValues(input)));

    //null on the left
    assertEquals(null, evaluate("value <> false", Tuple.ofValues(input)));
    assertEquals(null, evaluate("value = false", Tuple.ofValues(input)));
    assertEquals(null, evaluate("value and true", Tuple.ofValues(input)));
    assertEquals(null, evaluate("value && true", Tuple.ofValues(input)));
  }


  @Test
  public void logicalOrReturnsResultIfEitherSideIsNotNull() {
    inputStruct = Struct.of("foo", Nullable.BOOLEAN, "bar", Nullable.BOOLEAN);

    //bar is null
    assertEquals(Boolean.TRUE, evaluate("foo or bar", Tuple.ofValues((Struct) inputStruct, true)));
    assertEquals(Boolean.FALSE, evaluate("foo or bar", Tuple.ofValues((Struct) inputStruct, false)));

    assertEquals(Boolean.TRUE, evaluate("foo or bar", Tuple.ofValues((Struct) inputStruct, false, true)));
    //foo is null
    assertEquals(Boolean.TRUE, evaluate("foo or bar", Tuple.ofValues((Struct) inputStruct, null, true)));
    assertEquals(Boolean.FALSE, evaluate("foo or bar", Tuple.ofValues((Struct) inputStruct, null, false)));

    //foo and bar are null then result is null
    assertEquals(null, evaluate("foo or bar", Tuple.ofValues((Struct) inputStruct)));
    assertEquals(Nullable.BOOLEAN, realized.getResultType());
  }

  @Test
  public void canCompareUnrelatedObjects() throws Exception {
    inputStruct = Struct.of("foo", Nullable.INTEGER, "bar", Types.ANYTHING);
    assertEquals(Boolean.TRUE, evaluate("foo = bar", Tuple.ofValues((Struct) inputStruct, 4L, 4L)));
    assertEquals(Boolean.FALSE, evaluate("foo <> bar", Tuple.ofValues((Struct) inputStruct, 4L, 4L)));
    assertEquals(Boolean.FALSE, evaluate("foo = bar", Tuple.ofValues((Struct) inputStruct, 4L, 4D)));
    assertEquals(Boolean.TRUE, evaluate("foo <> bar", Tuple.ofValues((Struct) inputStruct, 4L, 4D)));
    assertNull(evaluate("foo <> bar", Tuple.ofValues((Struct) inputStruct, null, 4L)));
    assertEquals(Nullable.BOOLEAN, realized.getResultType());
  }

  @Test
  public void comparingNullAlwaysReturnsNull() throws Exception {
    inputStruct = Struct.of("foo", Nullable.INTEGER, "bar", Nullable.INTEGER);
    assertEquals(Boolean.FALSE, evaluate("foo <> bar", Tuple.ofValues((Struct) inputStruct, 4L, 4L)));
    assertNull(evaluate("foo <> bar", Tuple.ofValues((Struct) inputStruct, 4L, null)));
    assertEquals("foo <> bar", expr.toSource());
    assertNull(evaluate("foo <> bar", Tuple.ofValues((Struct) inputStruct, null, 4L)));
    assertNull(evaluate("foo <> bar", Tuple.ofValues((Struct) inputStruct, null, null)));

    // != is the same as <>
    assertEquals(Boolean.FALSE, evaluate("foo != bar", Tuple.ofValues((Struct) inputStruct, 4L, 4L)));
    assertNull(evaluate("foo != bar", Tuple.ofValues((Struct) inputStruct, 4L, null)));
    assertEquals("foo != bar", expr.toSource());
    assertNull(evaluate("foo != bar", Tuple.ofValues((Struct) inputStruct, null, 4L)));
    assertNull(evaluate("foo != bar", Tuple.ofValues((Struct) inputStruct, null, null)));

    assertNull(evaluate("foo > 20", Tuple.ofValues((Struct) inputStruct, null, null)));
    assertNull(evaluate("20 > foo", Tuple.ofValues((Struct) inputStruct, null, null)));

    // throw in some tests with and without a int -> float conversion of nulls.
    assertNull(evaluate("foo = 4", Tuple.ofValues((Struct) inputStruct, null, null)));
    assertNull(evaluate("foo = 4.0", Tuple.ofValues((Struct) inputStruct, null, null)));
    assertNull(evaluate("foo != 4", Tuple.ofValues((Struct) inputStruct, null, null)));
    assertNull(evaluate("foo != 4.0", Tuple.ofValues((Struct) inputStruct, null, null)));
    assertNull(evaluate("foo >= 4", Tuple.ofValues((Struct) inputStruct, null, null)));
    assertNull(evaluate("foo >= 4.0", Tuple.ofValues((Struct) inputStruct, null, null)));
  }


  @Test
  public void testSomeNullableMaths() throws Exception {
    inputStruct = Struct.of("foo", Nullable.INTEGER, "bar", Nullable.INTEGER);
    assertNull(evaluate("foo + bar", Tuple.ofValues((Struct) inputStruct, null, 4L)));
    assertNull(evaluate("foo + bar", Tuple.ofValues((Struct) inputStruct, null, null)));
    assertEquals(3L, evaluate("foo + bar", Tuple.ofValues((Struct) inputStruct, 2L, 1L)));
    assertEquals(Nullable.INTEGER, realized.getResultType());
  }

  @Test
  public void propertyAccessNeedsStructInput() throws Exception {
    evaluate("(1 + 3).value", null);
    assertThat(realizationProblems, contains(isProblem(Severity.ERROR, TypeProblems.class, "notStruct")));
    assertThat(realizationProblems, contains(isProblemShown(Severity.ERROR,
        is("Expected expression '(1 + 3).value' to be a Struct, but got Integer instead"))));
  }

  @Test
  public void propertyAccessReceiverCanBeNullable() throws Exception {
    Struct inner = Struct.of("bar", Types.INTEGER);
    Struct struct = Struct.of("foo", Nullable.of(inner));
    inputStruct = Nullable.of(struct);
    // this expression should return a nullable struct
    assertNull(evaluate("if_then_else(true, foo, foo)", null));
    assertTrue(Nullable.is(realized.getResultType()));
    assertTrue(Nullable.strip(realized.getResultType()) instanceof Struct);
    // we should then be able to dereference that nullable struct
    assertNull(evaluate("if_then_else(true, foo, foo).bar", null));
    assertThat(realizationProblems, empty());

    assertEquals(Nullable.INTEGER, realized.getResultType());
    assertNull(realized.evaluate(Tuple.ofValues(struct, new Object[] {null})));
    assertEquals(1L, realized.evaluate(Tuple.ofValues(struct, Tuple.ofValues(inner, 1L))));
  }

  @Test
  public void propertyAccessWithTrailingSelectAllNotAllowedOutSideOfAStruct() throws Exception {
    evaluate("foo.*", tuple("{foo: {bar: 1}}"));
    assertThat(
        realizationProblems,
        contains(hasAncestorProblem(Matchers.equalTo(
            ExpressionProblems.get().pointlessSelectAll((PropertyAccess) parse("foo.*"))
        )))
    );

    evaluate("[foo.*]", tuple("{foo: {bar: 1}}"));
    assertThat(
        realizationProblems,
        contains(hasAncestorProblem(Matchers.equalTo(
            ExpressionProblems.get().pointlessSelectAll((PropertyAccess) parse("foo.*"))
        )))
    );

    evaluate("{[foo.*]}", tuple("{foo: {bar: 1}}"));
    assertThat(
        realizationProblems,
        contains(hasAncestorProblem(Matchers.equalTo(
            ExpressionProblems.get().pointlessSelectAll((PropertyAccess) parse("foo.*"))
        )))
    );
  }


  @Test
  public void functionsAreLookedUpFromTheProjectsFunctionSet() throws Exception {
    RiskscapeFunction plusFunction = new BinaryFunction<>(Types.INTEGER, Types.INTEGER, Types.TEXT, (a, b) -> 22L);
    project.getFunctionSet().add(plusFunction.identified("plus"));

    assertEquals(22L, evaluate("plus(1, 1)", null));
  }

  @Test
  public void functionsWithKeywordArgumentsGetArgsInTheRightOrder() throws Exception {
    RiskscapeFunction function = keywordArgFunction(ArgumentList.fromArray(
        new FunctionArgument("fooarg", Types.TEXT),
        new FunctionArgument("bararg", Types.INTEGER),
        new FunctionArgument("bazarg", Types.FLOATING)
    ));

    project.getFunctionSet().add(function.identified("my_function", null, null, null));

    assertEquals(
        Arrays.asList("some text", 1L, 3.0D),
        evaluate("my_function(fooarg: 'some text', bararg: 1, bazarg: 3.0)", null)
    );

    assertEquals(
        Arrays.asList("more foo", 22L, 42.0D),
        evaluate("my_function(bararg: 22, bazarg: 42.0, fooarg: 'more foo')", null)
    );

    // even if keywords are quoted
    assertEquals(
        Arrays.asList("more foo", 22L, 42.0D),
        evaluate("my_function(\"bararg\": 22, \"bazarg\": 42.0, \"fooarg\": 'more foo')", null)
    );
  }

  @Test
  public void functionsWithOptionalKeywordArgumentsGetArgsInTheRightOrder() throws Exception {
    RiskscapeFunction function = keywordArgFunction(ArgumentList.fromArray(
        new FunctionArgument("fooarg", Types.TEXT),
        new FunctionArgument("bararg", Nullable.of(Types.INTEGER)),
        new FunctionArgument("bazarg", Nullable.of(Types.FLOATING))
    ));

    project.getFunctionSet().add(function.identified("my_function", null, null, null));

    assertEquals(
        Arrays.asList("some text", null, null), evaluate("my_function(fooarg: 'some text')", null)
    );

    assertEquals(
        Arrays.asList("some text", 1L, null), evaluate("my_function(fooarg: 'some text', bararg: 1)", null)
    );

    assertEquals(
        Arrays.asList("some text", null, 0.5D), evaluate("my_function(fooarg: 'some text', bazarg: 0.5)", null)
    );

    assertEquals(
        Arrays.asList("foo", 42L, null), evaluate("my_function(bararg: 42, fooarg: 'foo')", null)
    );

    assertEquals(
        Arrays.asList("foo", 42L, 99.9D),
        evaluate("my_function(bararg: 42, fooarg: 'foo', bazarg: 99.9)", null)
    );
    assertEquals(
        Arrays.asList("foo", 42L, 99.9D),
        evaluate("my_function(bazarg: 99.9, bararg: 42, fooarg: 'foo')", null)
    );
  }

  @Test
  public void listsWithConstantElementsCanBeRealized() throws Exception {
    assertEquals(Arrays.asList(1L, 2L, 3L), evaluate("[1, 2, 3]", null));
    assertEquals(Arrays.asList("foo", "bar", "baz"), evaluate("['foo', 'bar', 'baz']", null));
  }

  @Test
  public void anEmptylistCanBeRealized() throws Exception {
    assertEquals(Collections.emptyList(), evaluate("[]", null));
    assertTrue(realized.getResultType().find(EmptyList.class).isPresent());
  }

  @Test
  public void aListWithMixedTypesCanBeRealized() throws Exception {
    assertEquals(Arrays.asList(1L, "foo", 3.0D), evaluate("[1, 'foo', 3.0]", null));
    assertEquals(Types.ANYTHING, realized.getResultType().find(RSList.class).get().getContainedType());
  }

  @Test
  public void aListWithMixedNumericTypesCanBeRealized() throws Exception {
    assertEquals(Arrays.asList(1D, 4D, 3D), evaluate("[1, 4, 3.0]", null));
    assertEquals(Types.FLOATING, realized.getResultType().find(RSList.class).get().getContainedType());

    Struct type = Struct.of("foo", Types.INTEGER, "bar", Nullable.FLOATING, "baz", Types.FLOATING);
    assertEquals(Arrays.asList(1D, null, 4D), evaluate("[foo, bar, baz]", Tuple.ofValues(type, 1, null, 4D)));
    assertEquals(Nullable.FLOATING, realized.getResultType().find(RSList.class).get().getContainedType());

    assertEquals(Arrays.asList(1D, 4D), evaluate("[foo, baz]", Tuple.ofValues(type, 1, null, 4D)));
    assertEquals(Types.FLOATING, realized.getResultType().find(RSList.class).get().getContainedType());
  }

  @Test
  public void aListWithMixedButCommonBaseTypesCanBeRealized() throws Exception {
    inputStruct = Struct.of(
        "foo", new WithinSet(Types.TEXT, "foo", "bar"),
        "bar", new WithinSet(Types.TEXT, "baz")
    );

    assertEquals(
        Arrays.asList("foo", "baz", "bar"),
        evaluate(
            "[foo, bar, 'bar']",
            Tuple.ofValues((Struct) inputStruct, "foo", "baz")
        )
    );
    assertEquals(RSList.create(Types.TEXT), realized.getResultType());
  }

  @Test
  public void aListWithNullableTypesCanBeRealized() throws Exception {
    assertEquals(Arrays.asList("a", null, "c"), evaluate("['a', null, 'c']", null));
    assertEquals(Nullable.TEXT, realized.getResultType().find(RSList.class).get().getContainedType());

    assertEquals(Arrays.asList(1L, null, 3L), evaluate("[1, null, int(3.0)]", null));
    assertEquals(Nullable.INTEGER, realized.getResultType().find(RSList.class).get().getContainedType());

    // nullable type doesn't match the other list elements
    assertEquals(Arrays.asList(1L, null, 3L), evaluate("[1, null_of('text'), 3]", null));
    assertEquals(Nullable.ANYTHING, realized.getResultType().find(RSList.class).get().getContainedType());

    // no common element type
    assertEquals(Arrays.asList("a", null, 3L), evaluate("['a', null_of('integer'), 3]", null));
    assertEquals(Nullable.ANYTHING, realized.getResultType().find(RSList.class).get().getContainedType());
  }

  @Test
  public void nullArgsToOperatorsAreWrappedMagically() throws Exception {
    inputStruct = Struct.of("foo", Nullable.INTEGER, "bar", Types.INTEGER);

    assertEquals(7L, evaluate("foo + bar", Tuple.ofValues((Struct) inputStruct, 3L, 4L)));
    assertTrue(realized.getResultType().find(Nullable.class).isPresent());
    assertNull(evaluate("foo + bar", Tuple.ofValues((Struct) inputStruct, 3L, null)));
  }

  @Test
  public void functionsThatAcceptAnythingMatchAnything() throws Exception {
    RiskscapeFunction foo = new UnaryFunction<Object>(Types.ANYTHING, Types.INTEGER, x ->
      (long) x.toString().length());

    project.getFunctionSet().add(foo.identified("promiscuous"));

    assertEquals(1L, evaluate("promiscuous(1)", null));
    assertEquals(9L, evaluate("promiscuous([1, 2, 3])", null));
  }


  @Test
  public void canRealizeAnEmptyStruct() throws Exception {
    evaluate("{}", null);
    assertEquals(realized.getResultType(), Struct.EMPTY_STRUCT);
    assertEquals(new Tuple(Struct.EMPTY_STRUCT), evaluated);

  }

  @Test
  public void canRealizeASingleMemberStruct() throws Exception {
    evaluate("{foo: 'bar'}", null);
    Struct expected = Struct.of("foo", Types.TEXT);
    assertEquals(realized.getResultType(), expected);
    assertEquals(Tuple.ofValues(expected, "bar"), evaluated);
  }

  @Test
  public void canRealizeAManyMemberStruct() throws Exception {
    evaluate("{foo: 'bar', baz: 12}", null);
    Struct expected = Struct.of("foo", Types.TEXT, "baz", Types.INTEGER);
    assertEquals(realized.getResultType(), expected);
    assertEquals(Tuple.ofValues(expected, "bar", 12L), evaluated);
  }

  @Test
  public void repeatingAStructMemberGivesAnError() throws Exception {
    evaluate("{foo: 'bar', foo: 12}", null);
    StructDeclaration sd = expr.isA(StructDeclaration.class).get();
    assertThat(realizationProblems,
      contains(
        equalTo(ExpressionProblems.get().canNotReplaceMember("foo", sd.getMember(0), sd.getMember(1)))
      )
    );
  }

  @Test
  public void canRealizeAManyMemberStruct_AsSyntax() throws Exception {
    Struct inputType = Struct.of("height", Types.INTEGER, "cost_cents", Types.INTEGER);
    Tuple input = Tuple.ofValues(inputType, 2400L, 1000L);

    evaluate("{height as height, 0 as count, cost_cents / 100 as cost_dollars}", input);

    Struct expected = Struct.of("height", Types.INTEGER, "count", Types.INTEGER, "cost_dollars", Types.FLOATING);
    assertEquals(realized.getResultType(), expected);
    assertEquals(Tuple.ofValues(expected, 2400L, 0L, 10.0D), evaluated);
  }

  @Test
  public void canRealizeAManyMemberStruct_AsSyntax_InferredName() throws Exception {
    Struct inputType = Struct.of("height", Types.INTEGER, "cost_cents", Types.INTEGER);
    Tuple input = Tuple.ofValues(inputType, 2400L, 1000L);

    //height will infer attribute name from the source expression `height` since there is no `as`
    evaluate("{height, 0 as count, cost_cents / 100 as cost_dollars}", input);

    Struct expected = Struct.of("height", Types.INTEGER, "count", Types.INTEGER, "cost_dollars", Types.FLOATING);
    assertEquals(realized.getResultType(), expected);
    assertEquals(Tuple.ofValues(expected, 2400L, 0L, 10.0D), evaluated);
  }

  @Test
  public void structDeclarationImplicitNameRules() throws Exception {
    inputStruct = Struct.of(
      "foo", Struct.of(
        "bar", Types.TEXT,
        "baz", new WithinSet(Types.INTEGER, 1L, 2L, 3L),
        "nullable_bar", Nullable.TEXT
      )
    );

    project.getFunctionSet().add(
      RiskscapeFunction.create(this, Collections.EMPTY_LIST, Types.NOTHING, args -> null).identified("zeroarg")
    );

    project.getFunctionSet().add(
      RiskscapeFunction.create(this, Arrays.asList(Types.ANYTHING, Types.ANYTHING), Types.NOTHING, args -> null)
        .identified("binary")
    );

    IdentifiedFunction unary = new BaseRealizableFunction(ArgumentList.fromArray(), Types.ANYTHING) {
      @Override
      public ResultOrProblems<RiskscapeFunction> realize(RealizationContext context, FunctionCall functionCall,
          List<Type> argumentTypes) {
        return ResultOrProblems.of(
          RiskscapeFunction.create(this, argumentTypes, argumentTypes.get(0), args -> args.get(0))
        );
      }
    }.builtin("unary", Category.MISC);


    project.getFunctionSet().add(unary);
    Map<String, String> exprWithExpectedNames = ImmutableMap.<String, String>builder()
        // attribute/member path rules
        .put("foo", "foo")
        .put("foo.bar", "bar") // trailing member only

        // literal rules - use type's constructor name
        .put("1", "integer")
        .put("-0.33", "floating")
        .put("'bird is the word'", "text")
        .put("{some: 1, various: 'types'}", "struct")
        // for list, we append the element type
        .put("[1, 2, 3]", "list_integer")
        .put("[1, 'foo']", "list_anything")
        .put("[foo.bar]", "list_text") // bar is text, so that's our list type
        .put("[foo.baz]", "list_integer") // baz is a within set of int, we just use the underlying type
        .put("[foo.nullable_bar]", "list_text") // nullable gets stripped
        // a lambda is just lambda
        .put("(x) -> x", "lambda")


        // function call rules:
        // 1. use the function's name
        .put("zeroarg()", "zeroarg")
        .put("binary(foo, 'text')", "binary") //
         // 2. unary functions use the first arg's implicit name
        .put("unary(foo)", "unary_foo")
        .put("unary('hi')", "unary_text")
        .put("unary([1, 2, 3])", "unary_list_integer")
        //  3. when followed by a path, we use the path
        .put("unary({returned: {child: '1'}}).returned.child", "child")

        .build();

    for (Map.Entry<String, String> exprAndExpected : exprWithExpectedNames.entrySet()) {
      // check the implicit name method directly
      String implicitName = ExpressionRealizer.getImplicitName(realizationContext,
          realize(inputStruct, parse(exprAndExpected.getKey())), Collections.emptyList());

      assertThat(implicitName, equalTo(exprAndExpected.getValue()));

      // now build a struct
      String structExpression = "{" + exprAndExpected.getKey() + "}";
      realize(inputStruct, parse(structExpression));

      if (realized == null) {
        fail(Problem.debugString(this.realizationProblems));
      }

      Struct realizedStruct = realized.getResultType().find(Struct.class).get();
      assertThat(realizedStruct.getMembers().get(0).getKey(), equalTo(exprAndExpected.getValue()));
    }
  }

  @Test
  public void structDeclarationImplicitNamesAreAlwaysDistinct() throws Exception {
    assertThat(
      realize(Struct.EMPTY_STRUCT, parse("{1, 200, 'how', 333, 'are you?'}")).getResultType(),
      equalTo(
        Struct.builder()
          .add("integer", Types.INTEGER)
          .add("integer_2", Types.INTEGER)
          .add("text", Types.TEXT)
          .add("integer_3", Types.INTEGER)
          .add("text_2", Types.TEXT)
          .build()
      )
    );
  }

  @Test
  public void implicitNamingDoesNotErrorWhenDependenciesFailToRealize() {
    assertThat(realizer.realize(Struct.EMPTY_STRUCT, parse("{square_root('nine')}")), failedResult(
        isProblem(MissingFunctionException.class)
    ));
  }

  @Test
  public void repeatingAStructMemberGivesAnError_AsSyntax() throws Exception {
    evaluate("{'bar' as foo, 12 as foo}", null);
    StructDeclaration sd = expr.isA(StructDeclaration.class).get();
    assertThat(realizationProblems,
      contains(
        hasAncestorProblem(equalTo(
          ExpressionProblems.get().canNotReplaceMember("foo", sd.getMember(0), sd.getMember(1))
        ))
      )
    );
  }

  @Test
  public void selectAllAgainstAStructWithDuplicateMembersIsFine() throws Exception {
    assertEquals(
      tuple("{foo: 2}"),
      evaluate("{*, foo: 2}", tuple("{foo: 1}"))
    );

    // a slightly more complex example showing some replacement, some addition, and some splatting
    assertEquals(
      tuple("{bar: 2, baz: 3, foobar: 'great'}"),
      evaluate("{foo.*, 2 as bar, 'great' as foobar}", tuple("{foo: {bar: 1, baz: 3}}"))
    );
  }

  @Test
  public void selectAllAgainstANullableStruct() throws Exception {
    Struct inputType = Struct.of("foo", Nullable.of(Struct.of("bar", Types.TEXT, "baz", Types.TEXT)));

    // nullable, but with values
    evaluate("{foo.*}", Tuple.ofValues(inputType, new Object[] {tuple("{bar: 'some-bar', baz: 'all-baz'}")}));
    assertEquals(Struct.of("bar", Nullable.TEXT, "baz", Nullable.TEXT), realized.getResultType());
    assertEquals(Tuple.ofValues((Struct) realized.getResultType(), "some-bar", "all-baz"), evaluated);

    // nullable, and is null
    assertEquals(
      tuple("{bar: null_of('text'), baz: null_of('text')}"),
      evaluate("{foo.*}", Tuple.ofValues(inputType, new Object[] {null}))
    );
    assertEquals(Struct.of("bar", Nullable.TEXT, "baz", Nullable.TEXT), realized.getResultType());
  }

  @Test
  public void selectAllAgainstANestedNullableStruct() throws Exception {
    Struct inputType = struct("foo: struct(bar: nullable(struct(baz: text)))");
    // with a value
    evaluate("{foo.*}", tupleOfType(inputType, "{foo: {bar: {baz: 'hi'}}}"));
    assertEquals(struct("bar: nullable(struct(baz: text))"), realized.getResultType());
    assertEquals(tupleOfType(realized.getResultType(), "{bar: {baz: 'hi'}}"), evaluated);

    // with a null bar
    evaluate("{foo.*}", Tuple.ofValues(inputType,
        Tuple.ofValues(inputType.getMembers().get(0).getType().asStruct(), new Object[] {null})));

    assertEquals(Tuple.ofValues(inputType.getMembers().get(0).getType().asStruct(), new Object[] {null}), evaluated);
  }

  @Test
  public void selectAllAgainstAStructMemberAssignsInputScopeToTheMember() throws Exception {
    assertEquals(
        tuple("{foo: {cat: 1, dog: 2}}"),
        evaluate("{foo: *}", tuple("{cat: 1, dog: 2}"))
    );
  }

  @Test
  public void selectAllOnAStructMemberGivesAnError() throws Exception {
    // this is a pointless expression and ambiguous, so disallow it
    evaluate("{foo: {bar: true, baz: false}.*}", tuple("{cat: 1, dog: 2}"));

    Problem expectedProblem = ExpressionProblems.get().pointlessSelectAllInStruct(
      expr.isA(StructDeclaration.class).get().getMember(0)
    );

    assertThat(realizationProblems,
      contains(hasAncestorProblem(equalTo(expectedProblem)))
    );
  }

  @Test
  public void canIndexADeclaredStruct() throws Exception {
    evaluate("{foo: 'bar'}.foo", null);
    assertEquals(realized.getResultType(), Types.TEXT);
    assertEquals("bar", evaluated);
  }

  @Test
  public void canSelectAllEmptyToMakeEmpty() throws Exception {
    Tuple empty = Tuple.ofValues(Struct.EMPTY_STRUCT);
    assertEquals(empty, evaluate("{*}", empty));
  }

  @Test
  public void canSelectAllATupleToCopyIt() throws Exception {
    Struct expected = Struct.of("foo", Types.TEXT, "bar", Types.INTEGER);
    Tuple tuple = Tuple.ofValues(expected, "text", 1);
    assertEquals(tuple, evaluate("{*}", tuple));
  }

  @Test
  public void canUseSelectAllToPrependATuple() throws Exception {
    Struct inputStruct = Struct.of("foo", Types.TEXT, "bar", Types.INTEGER);
    Tuple inputTuple = Tuple.ofValues(inputStruct, "text", 1L);

    Tuple evaluated = (Tuple) evaluate("{*, cool: 'baz text', story: 2}", inputTuple);
    Tuple expected = Tuple.ofValues(evaluated.getStruct(), "text", 1L, "baz text", 2L);

    assertEquals(expected, evaluated);
  }

  @Test
  public void canUseSelectAllToInsertATuple() throws Exception {
    Struct inputStruct = Struct.of("foo", Types.TEXT, "bar", Types.INTEGER);
    Tuple inputTuple = Tuple.ofValues(inputStruct, "text", 1L);

    Tuple evaluated = (Tuple) evaluate("{cool: 'baz text', *, story: 2}", inputTuple);
    Tuple expected = Tuple.ofValues(evaluated.getStruct(), "baz text", "text", 1L, 2L);

    assertEquals(expected, evaluated);
  }

  @Test
  public void canUseSelectAllToAppendATuple() throws Exception {
    Struct inputStruct = Struct.of("foo", Types.TEXT, "bar", Types.INTEGER);
    Tuple inputTuple = Tuple.ofValues(inputStruct, "text", 1L);

    Tuple evaluated = (Tuple) evaluate("{cool: 'baz text', story: 2, *}", inputTuple);
    Tuple expected = Tuple.ofValues(evaluated.getStruct(), "baz text", 2L, "text", 1L);

    assertEquals(expected, evaluated);
  }

  @Test
  public void aSoloSelectAllYieldsInput() throws Exception {
    Struct inputStruct = Struct.of("foo", Types.TEXT);
    Tuple inputTuple = Tuple.ofValues(inputStruct, "bar");
    assertEquals(inputTuple, evaluate("*", inputTuple));
  }

  @Test
  public void canSelectAllSpecificMembersInToAStruct() throws Exception {
    Tuple inputTuple = tuple("{foo: {cool: 1, story: 'yes'}, bar: {cat: 0.2, dog: false}}");

    assertEquals(
        tuple("{cool: 1, story: 'yes'}"),
        evaluate("{foo.*}", inputTuple)
    );

    assertEquals(
        tuple("{cat: 0.2, dog: false}"),
        evaluate("{bar.*}", inputTuple)
    );

    assertEquals(
        tuple("{cool: 1, story: 'yes', cat: 0.2, dog: false}"),
        evaluate("{foo.*, bar.*}", inputTuple)
    );

    assertEquals(
        tuple("{cat: 0.2, dog: false, cool: 1, story: 'yes'}"),
        evaluate("{bar.*, foo.*}", inputTuple)
    );

    // try some various 'sandwiched' expressions
    assertEquals(
        tuple("{cat: 0.2, dog: false, baz: true}"),
        evaluate("{bar.*, true as baz}", inputTuple)
    );

    assertEquals(
        tuple("{cool: 'story', cat: 0.2, dog: false, baz: true}"),
        evaluate("{'story' as cool, bar.*, true as baz}", inputTuple)
    );

    assertEquals(
        tuple("{cool: 'story', baz: true, cat: 0.2, dog: false}"),
        evaluate("{'story' as cool, true as baz, bar.*}", inputTuple)
    );

    assertEquals(
        tuple("{cat: 0.2, dog: false, cool: 'story', baz: true}"),
        evaluate("{bar.*, 'story' as cool, true as baz}", inputTuple)
    );
  }

  @Test
  public void canSelectAllTheResultOfAnExpressionInToAStruct() throws Exception {
    assertEquals(
        tuple("{foo: 1}"),
        evaluate("{{foo: 1}.*}", Tuple.EMPTY_TUPLE)
    );
  }

  @Test
  public void canSelectAllTheIndexedResultOfAnExpressionInToAStruct() throws Exception {
    assertEquals(
        tuple("{baz: [1, 2, 3]}"),
        evaluate("{{bar: {baz: [1, 2, 3]}}.bar.*}", Tuple.EMPTY_TUPLE)
    );
  }

  @Test
  public void exceptionsAreCaughtAndWrapped() throws Exception {
    RiskscapeFunction plus = Mockito.mock(RiskscapeFunction.class);
    project.add(new IdentifiedFunction.Wrapping(plus, "plus", "", Resource.UNKNOWN_URI, Category.MISC));

    RuntimeException exception = new RuntimeException("bad times");
    RiskscapeFunction thrower = new BinaryFunction<>(Types.INTEGER, Types.INTEGER, Types.NOTHING, (a, b) -> {
      throw exception;
    });

    RealizableFunction mockRealizable = Mockito.mock(RealizableFunction.class);
    Mockito.when(plus.getRealizable()).thenReturn(Optional.of(mockRealizable));

    Mockito.when(mockRealizable.realize(
        Mockito.same(realizationContext),
        Mockito.any(),
        Mockito.any())
    )
    .thenReturn(ResultOrProblems.of(thrower));


    Tuple input = new Tuple(Struct.EMPTY_STRUCT);
    EvalException evalException = Assert.assertThrows(EvalException.class, () ->  evaluate("plus(1, 1)", input));
    assertSame(input, evalException.getInput());
    assertNotNull(evalException.getRealizedExpression());
    assertSame(exception, evalException.getCause());
  }

  @Test
  public void aNoArgLambdaCanBeRealized() throws Exception {
    Tuple scope = tuple("{foo: 1}");
    ScopedLambdaExpression scoped = (ScopedLambdaExpression) evaluate("() -> 1", scope);
    assertEquals(LambdaType.NO_ARGS.scoped(scope.getStruct()), realized.getResultType());

    assertEquals("() -> 1", scoped.getExpression().toSource());
    assertSame(scope, scoped.getScope());
  }

  @Test
  public void anNArgLambdaCanBeRealized() throws Exception {
    Tuple scope = tuple("{foo: 1}");
    ScopedLambdaExpression scoped = (ScopedLambdaExpression) evaluate("(foo, bar, baz) -> 1", scope);
    assertEquals(new LambdaType("foo", "bar", "baz").scoped(scope.getStruct()), realized.getResultType());

    assertEquals("(foo, bar, baz) -> 1", scoped.getExpression().toSource());
    assertSame(scope, scoped.getScope());
  }

  @Test
  public void evalExceptionContainsRQL() {
    EvalException ee = assertThrows(EvalException.class, () -> evaluate("float('cat')", null));
    assertThat(render(ee), is("Failed to evaluate `float('cat')`\n"
        + "  - Failed to parse number from 'cat'"));

    DummyFunction boomFunction = new DummyFunction("boom", Lists.newArrayList(Types.TEXT)){
      @Override
      public Object call(List<Object> args) {
        throw new RuntimeException("boom");
      }

      @Override
      public Type getReturnType() {
        return Types.FLOATING;
      }

    };
    project.getFunctionSet().add(boomFunction);
    ee = assertThrows(EvalException.class, () -> evaluate("max(1.4, boom('cat'))", null));
    assertThat(render(ee), startsWith("Failed to evaluate `max(1.4, boom('cat'))`"));
    assertThat(render(ee.getCause()), startsWith("Failed to evaluate arg 1 `max(1.4, boom('cat'))`"));

    ee = assertThrows(EvalException.class, () -> evaluate("1.4 + boom('cat')", null));
    assertThat(render(ee), startsWith("Failed to evaluate `1.4 + boom('cat')`"));
    assertThat(render(ee.getCause()), startsWith("Failed to evaluate `boom('cat')`"));
  }

  @Test
  public void realizerParseMethodThrowsInsteadOfResultOr() throws Exception {
    assertThrows(LexerException.class, () -> realizer.parse("&"));
    assertThrows(ParseException.class, () -> realizer.parse("123 foo"));
  }

  @Test
  public void multipleEqualStructsInAnExpressionUseTheSameStructObject() throws Exception {
    List<?> values = (List<?>) evaluate("[{foo: 1}, {foo: 2}, {foo: 3}]", Tuple.EMPTY_TUPLE);
    Tuple first = (Tuple) values.get(0);
    for (Object object : values) {
      assertSame(((Tuple) object).getStruct(), first.getStruct());
    }

    // and again, but now it reuses the same one as last time
    values = (List<?>) evaluate("[{foo: 1}, {foo: 2}, {foo: 3}]", Tuple.EMPTY_TUPLE);
    for (Object object : values) {
      assertSame(((Tuple) object).getStruct(), first.getStruct());
    }

    // same if we have nested structs
    Tuple tuple = (Tuple) evaluate("{cool: {foo: 1}, story: {foo: 2}}", Tuple.EMPTY_TUPLE);
    assertSame(tuple.fetchChild("cool").getStruct(), tuple.fetchChild("story").getStruct());

    // not the same if they have the same keys but different types
    tuple = (Tuple) evaluate("{cool: {foo: 1}, story: {foo: 'bar'}}", Tuple.EMPTY_TUPLE);
    assertSame(first.getStruct(), tuple.fetchChild("cool").getStruct());
    assertNotSame(first.getStruct(), tuple.fetchChild("story").getStruct());

  }

  @Test
  public void realizingBadExpressionIsAProblem() {
    assertThat(realizer.realize(Struct.EMPTY_STRUCT, "2^3"), failedResult(
        equalTo(LexerProblems.get().unexpectedCharacter(new SourceLocation(1, 1, 2), '^'))
    ));
  }

  @Test
  public void willMakeArgsNullSafeAndCoerceTypes() {
    Struct functionArg = Struct.of("v1", Types.FLOATING, "v2", Types.FLOATING);
    DummyFunction dummy = new DummyFunction(Arrays.asList(functionArg)) {
      @Override
      public Type getReturnType() {
        return Types.FLOATING;
      }

      @Override
      public Object call(List<Object> args) {
        Tuple arg1 = (Tuple)args.get(0);
        return (Double)arg1.fetch("v1") * (Double)arg1.fetch("v2");
      }

    };
    project.getFunctionSet().add(dummy);

    Struct inputArg = Struct.of("v1", Types.INTEGER, "v2", Types.INTEGER);
    Struct inputType = Struct.of("value", Nullable.of(inputArg));

    assertThat(evaluate("dummy(value)", Tuple.of(inputType)), is(nullValue()));
    assertThat(realized.getResultType(), is(Nullable.FLOATING));

    assertThat(evaluate("dummy(value)", Tuple.ofValues(inputType, Tuple.ofValues(inputArg, 4L, 3L))), is(12D));
    assertThat(realized.getResultType(), is(Nullable.FLOATING));
  }
}
