/*
 * 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.defaults.interp;

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 org.junit.Before;
import org.junit.Test;

import nz.org.riskscape.defaults.interp.BilinearContinuousFunctionType.XY;
import nz.org.riskscape.engine.ArgsProblems;
import nz.org.riskscape.engine.DummyFunction;
import nz.org.riskscape.engine.Matchers;
import nz.org.riskscape.engine.Tuple;
import nz.org.riskscape.engine.function.ArgumentList;
import nz.org.riskscape.engine.function.FunctionArgument;
import nz.org.riskscape.engine.rl.BaseExpressionRealizerTest;
import nz.org.riskscape.engine.rl.DefaultOperators;
import nz.org.riskscape.engine.rl.LanguageFunctions;
import nz.org.riskscape.engine.rl.LogicFunctions;
import nz.org.riskscape.engine.rl.MathsFunctions;
import nz.org.riskscape.engine.rl.agg.Accumulator;
import nz.org.riskscape.engine.rl.agg.RealizedAggregateExpression;
import nz.org.riskscape.engine.types.EmptyList;
import nz.org.riskscape.engine.types.Nullable;
import nz.org.riskscape.engine.types.RSList;
import nz.org.riskscape.engine.types.ScopedLambdaType;
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.problem.Problems;
import nz.org.riskscape.problem.ResultOrProblems;
import nz.org.riskscape.rl.ast.FunctionCall;

public class BilinearContinuousFunctionsTest extends BaseExpressionRealizerTest {


  ArgumentList arguments = new CreateBilinearContinuousFunction().asFunction().getArguments();
  ArgumentList applyArguments = new ApplyContinuousFunction().asFunction().getArguments();

  @Before
  public void setup() {
    project.getFunctionSet().add(new ApplyContinuousFunction().asFunction().identified("apply_continuous"));
    project.getFunctionSet().add(new CreateBilinearContinuousFunction().asFunction()
        .identified("create_continuous_2d"));
    project.getFunctionSet().insertFirst(new DefaultOperators());
    project.getFunctionSet().addAll(MathsFunctions.FUNCTIONS);
    project.getFunctionSet().addAll(LanguageFunctions.FUNCTIONS);
    project.getFunctionSet().addAll(LogicFunctions.LOGIC_FUNCTIONS);
  }

  @Test
  public void canBuildAndApplyABasicFunctionOnlyVariesX() throws Exception {
    // we create a curve that only varies with x. this lets us check the interpolation
    // on that axis in isolation
    evaluate("create_continuous_2d([1, 3, 5, 7, 9], [1], (x, y) -> (x/2) + 0.5)", tuple("{}"));
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // lets test some points that we have
    assertThat(evaluate("apply_continuous(value, 1, 1)", tuple), equalTo(1.0D));
    assertThat(realized.getResultType(), is(Types.FLOATING));
    assertThat(evaluate("apply_continuous(value, 1, 2)", tuple), equalTo(1.0D));

    assertThat(evaluate("apply_continuous(value, 3, 1)", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 3, 2)", tuple), equalTo(2.0D));

    assertThat(evaluate("apply_continuous(value, 5, 1)", tuple), equalTo(3.0D));
    assertThat(evaluate("apply_continuous(value, 5, 2)", tuple), equalTo(3.0D));

    // now some interpolated values
    assertThat(evaluate("apply_continuous(value, 2, 1)", tuple), equalTo(1.5D));
    assertThat(evaluate("apply_continuous(value, 2, 2)", tuple), equalTo(1.5D));
    assertThat(evaluate("apply_continuous(value, 4, 1)", tuple), equalTo(2.5D));
    assertThat(evaluate("apply_continuous(value, 4, 2)", tuple), equalTo(2.5D));

    // outside of sample range
    // low values
    assertThat(evaluate("apply_continuous(value, 0, 0)", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, -10, -10)", tuple), equalTo(1.0D));
    // high values we expect max_x/2 + 0.5 = 5.0
    assertThat(evaluate("apply_continuous(value, 10, 10)", tuple), equalTo(5.0D));
    assertThat(evaluate("apply_continuous(value, 20, 20)", tuple), equalTo(5.0D));
  }

  @Test
  public void canBuildAndApplyABasicStructFunctionOnlyVariesX() throws Exception {
    // we create a curve that only varies with x. this lets us check the interpolation
    // on that axis in isolation
    evaluate("create_continuous_2d([1, 3, 5, 7, 9], [1], (x, y) -> {a: (x/2) + 0.5, b: x * 2})", tuple("{}"));
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    Struct expectedReturnType = Struct.of("a", Types.FLOATING, "b", Types.FLOATING);
    assertThat(functionType.getReturnType(), equalTo(expectedReturnType));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // lets test some points that we have
    assertThat(evaluate("apply_continuous(value, 1, 1)", tuple), equalTo(tuple("{a: 1.0, b: 2.0}")));
    assertThat(realized.getResultType(), equalTo(expectedReturnType));
    assertThat(evaluate("apply_continuous(value, 1, 2)", tuple), equalTo(tuple("{a: 1.0, b: 2.0}")));

    assertThat(evaluate("apply_continuous(value, 9, 2)", tuple), equalTo(tuple("{a: 5.0, b: 18.0}")));

    // interpolated
    assertThat(evaluate("apply_continuous(value, 6, 2)", tuple), equalTo(tuple("{a: 3.5, b: 12.0}")));
  }

  @Test
  public void canCompressFloatingDataPoints() throws Exception {
    evaluate("""
        create_continuous_2d(
          [1, 2, 3],
          [4, 5, 6],
          (x, y) -> x + y,
          options: {compress: true}
        )
        """, tuple("{}"));

    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // on the boundaries
    assertThat(evaluate("apply_continuous(value, 1, 4)", tuple), equalTo(5.0D));
    assertThat(evaluate("apply_continuous(value, 3, 6)", tuple), equalTo(9.0D));
    // interpolated
    assertThat(evaluate("apply_continuous(value, 1.5, 4.5)", tuple), equalTo(6.0D));

    // sneak behind the scenes and check what is in our function
    float[] compressedArray = (float[]) function.values;
    // 0 and 1 are set
    assertThat(compressedArray[0], is(5.0f));
    assertThat(compressedArray[1], is(6.0F));

    // [2] is not (this would be x=3, y=4)
    assertThat(compressedArray[2], is(Float.NaN));

    // fourth is set (x=1, y=5)
    assertThat(compressedArray[3], is(6.0F));

    // last is set (x=3, y=6)
    assertThat(compressedArray[8], is(9.0F));
  }

  @Test
  public void canBuildAFunctionThatReturnsNullableValue() throws Exception {
    evaluate("""
        create_continuous_2d(
          [1, 2, 3],
          [4, 5, 6],
          (x, y) -> if(x = 1.0 or y = 4.0, null_of('floating'), x + y)
        )
        """, tuple("{}"));
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Nullable.FLOATING));

    // confirm it doesn't blow up, this is not an exhaustive test and tbh I'm not sure how it should behave with nulls
    // so for now this is enough to unblock us
    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);
    assertThat(evaluate("apply_continuous(value, 1, 4)", tuple), nullValue());
    assertThat(evaluate("apply_continuous(value, 3, 6)", tuple), equalTo(9.0D));
  }

  @Test
  public void canBuildAFunctionThatReturnsStructWithNullableMembers() throws Exception {
    evaluate("""
        create_continuous_2d(
          [1, 2, 3],
          [4, 5, 6],
          (x, y) -> {xy: if(x = 1.0 or y = 4.0, null_of('floating'), x + y)}
        )
        """, tuple("{}"));

    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    Struct expectedReturnType = Struct.of("xy", Nullable.FLOATING);
    assertThat(functionType.getReturnType(), equalTo(expectedReturnType));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // on the boundaries
    assertThat(evaluate("apply_continuous(value, 1, 4)", tuple), equalTo(tuple("{xy: null_of('floating')}")));
    assertThat(evaluate("apply_continuous(value, 3, 6)", tuple),
    equalTo(tupleOfType(expectedReturnType, "{xy: 9.0}")));
  }

  @Test
  public void canCompressStructDataPoints() throws Exception {
    evaluate("""
        create_continuous_2d(
          [1, 2, 3],
          [4, 5, 6],
          (x, y) -> {a: x + y, b: int(x + y * 2)}, # even if we cast to int, it is still returned as a double
          options: {compress: true}
        )
        """, tuple("{}"));

    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    Struct expectedReturnType = Struct.of("a", Types.FLOATING, "b", Types.FLOATING);
    assertThat(functionType.getReturnType(), equalTo(expectedReturnType));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // on the boundaries
    assertThat(evaluate("apply_continuous(value, 1, 4)", tuple), equalTo(tuple("{a: 5.0, b: 9.0}")));
    assertThat(evaluate("apply_continuous(value, 3, 6)", tuple), equalTo(tuple("{a: 9.0, b: 15.0}")));
    // interpolated
    assertThat(evaluate("apply_continuous(value, 1.5, 4.5)", tuple), equalTo(tuple("{a: 6.0, b: 10.5}")));

    // sneak behind the scenes and check what is in our function
    float[] compressedArray = (float[]) function.values;
    // 0 and 1 are set
    assertThat(compressedArray[0], is(5.0f));
    assertThat(compressedArray[1], is(9.0f));
    assertThat(compressedArray[2], is(6.0F));
    assertThat(compressedArray[3], is(10.0F));

    // [2] is not (this would be x=3, y=4)
    assertThat(compressedArray[4], is(Float.NaN));
    assertThat(compressedArray[5], is(Float.NaN));

    // fourth is set (x=1, y=5)
    assertThat(compressedArray[6], is(6.0F));
    assertThat(compressedArray[7], is(11.0F));

    // last is set (x=3, y=6)
    assertThat(compressedArray[16], is(9.0F));
    assertThat(compressedArray[17], is(15.0F));
  }

  @Test
  public void createFailsWithCompressIfFunctionReturnsStructWithNullableMembers() throws Exception {
    evaluate("""
        create_continuous_2d(
          [1, 2, 3],
          [4, 5, 6],
          (x, y) -> {res: null_of('floating')},
          options: {compress: true}
        )
        """, tuple("{}"));

    // it's a nasty error, just check it doesn't throw
    assertThat(realizationProblems, not(empty()));
  }

  @Test
  public void createFailsWithCompressIfFunctionReturnsNullable() throws Exception {
    evaluate("""
        create_continuous_2d(
          [1, 2, 3],
          [4, 5, 6],
          (x, y) -> null_of('floating'),
          options: {compress: true}
        )
        """, tuple("{}"));

    // it's a nasty error, just check it doesn't throw
    assertThat(realizationProblems, not(empty()));
  }

  @Test
  public void canBuildAndApplyABasicFunctionOnlyVariesXWithLog() throws Exception {
    // we create a curve that only varies with x. this lets us check the interpolation
    // on that axis in isolation
    // note that we're using a exp(x) scaling on the x axis when building the curve to make it easier
    // to see that the log scaling has been applied
    evaluate("create_continuous_2d(map([1, 3, 5, 7, 9], x -> exp(x)), [1], "
        + "(x, y) -> (log(x)/2) + 0.5, {apply_log_to_x: true})", tuple("{}"));
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // lets test some points that we have
    assertThat(evaluate("apply_continuous(value, exp(1), 1)", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, exp(1), 2)", tuple), equalTo(1.0D));

    assertThat(evaluate("apply_continuous(value, exp(3), 1)", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, exp(3), 2)", tuple), equalTo(2.0D));

    assertThat(evaluate("apply_continuous(value, exp(5), 1)", tuple), equalTo(3.0D));
    assertThat(evaluate("apply_continuous(value, exp(5), 2)", tuple), equalTo(3.0D));

    // now some interpolated values
    assertThat(evaluate("apply_continuous(value, exp(2), 1)", tuple), equalTo(1.5D));
    assertThat(evaluate("apply_continuous(value, exp(2), 2)", tuple), equalTo(1.5D));
    assertThat(evaluate("apply_continuous(value, exp(4), 1)", tuple), equalTo(2.5D));
    assertThat(evaluate("apply_continuous(value, exp(4), 2)", tuple), equalTo(2.5D));

    // outside of sample range
    // low values
    assertThat(evaluate("apply_continuous(value, exp(0), 0)", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, exp(-10), -10)", tuple), equalTo(1.0D));
    // high values we expect max_x/2 + 0.5 = 5.0
    assertThat(evaluate("apply_continuous(value, exp(10), 10)", tuple), equalTo(5.0D));
    assertThat(evaluate("apply_continuous(value, exp(20), 20)", tuple), equalTo(5.0D));

    // again with no log scaling
    evaluate("create_continuous_2d(map([1, 3, 5, 7, 9], x -> exp(x)), [1], "
        + "(x, y) -> (log(x)/2) + 0.5)", tuple("{}"));
    assertThat(evaluated, isA(BilinearContinuousState.class));

    function = (BilinearContinuousState) evaluated;
    functionType = (BilinearContinuousFunctionType) realized.getResultType();
    tuple = Tuple.ofValues(functionType.asStruct(), function);

    // lets test some points that we have, we expect these to be the same
    assertThat(evaluate("apply_continuous(value, exp(1), 1)", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, exp(1), 2)", tuple), equalTo(1.0D));

    assertThat(evaluate("apply_continuous(value, exp(3), 1)", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, exp(3), 2)", tuple), equalTo(2.0D));

    assertThat(evaluate("apply_continuous(value, exp(5), 1)", tuple), equalTo(3.0D));
    assertThat(evaluate("apply_continuous(value, exp(5), 2)", tuple), equalTo(3.0D));

    // now some interpolated values (should differ to results with log scaling
    assertThat((double)evaluate("apply_continuous(value, exp(2), 1)", tuple), closeTo(1.26894D, 0.0001D));
    assertThat((double)evaluate("apply_continuous(value, exp(2), 2)", tuple), closeTo(1.26894D, 0.0001D));
    assertThat((double)evaluate("apply_continuous(value, exp(4), 1)", tuple), closeTo(2.26894D, 0.0001D));
    assertThat((double)evaluate("apply_continuous(value, exp(4), 2)", tuple), closeTo(2.26894D, 0.0001D));

    // outside of sample range these should be the same as well.
    // low values
    assertThat(evaluate("apply_continuous(value, exp(0), 0)", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, exp(-10), -10)", tuple), equalTo(1.0D));
    // high values we expect max_x/2 + 0.5 = 5.0
    assertThat(evaluate("apply_continuous(value, exp(10), 10)", tuple), equalTo(5.0D));
    assertThat(evaluate("apply_continuous(value, exp(20), 20)", tuple), equalTo(5.0D));
  }

  @Test
  public void canBuildAndApplyABasicFunctionOnlyVariesY() throws Exception {
    // we create a curve that only varies with y. this lets us check the interpolation
    // on that axis in isolation
    evaluate("create_continuous_2d([1], [1, 2, 3, 4, 5], (x, y) -> min(2, (y/2) + 0.5))", tuple("{}"));
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // lets test some points that we have
    assertThat(evaluate("apply_continuous(value, 1, 1)", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, 2, 1)", tuple), equalTo(1.0D));

    assertThat(evaluate("apply_continuous(value, 1, 3)", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 2, 3)", tuple), equalTo(2.0D));

    assertThat(evaluate("apply_continuous(value, 1, 5)", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 2, 5)", tuple), equalTo(2.0D));

    // now some interpolated values
    assertThat(evaluate("apply_continuous(value, 1, 2)", tuple), equalTo(1.5D));
    assertThat(evaluate("apply_continuous(value, 2, 2)", tuple), equalTo(1.5D));
    assertThat(evaluate("apply_continuous(value, 1, 4)", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 2, 4)", tuple), equalTo(2.0D));

    // outside of sample range
    // low values
    assertThat(evaluate("apply_continuous(value, 0, 0)", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, -10, -10)", tuple), equalTo(1.0D));
    // high values we expect max_y/2 + 0.5 = 3.0
    assertThat(evaluate("apply_continuous(value, 10, 10)", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 20, 20)", tuple), equalTo(2.0D));
  }

  @Test
  public void canBuildAndApplyABasicFunctionOnlyVariesYWithLog() throws Exception {
    // we create a curve that only varies with y. this lets us check the interpolation
    // on that axis in isolation
    // note that we're using a exp(y) scaling on the y axis when building the curve to make it easier
    // to see that the log scaling has been applied
    evaluate("create_continuous_2d([1], map([1, 3, 5, 7, 9], y -> exp(y)), "
        + "(x, y) -> min(2, (log(y)/2) + 0.5), {apply_log_to_y: true})", tuple("{}"));
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // lets test some points that we have
    assertThat(evaluate("apply_continuous(value, 1, exp(1))", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, 2, exp(1))", tuple), equalTo(1.0D));

    assertThat(evaluate("apply_continuous(value, 1, exp(3))", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 2, exp(3))", tuple), equalTo(2.0D));

    assertThat(evaluate("apply_continuous(value, 1, exp(5))", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 2, exp(5))", tuple), equalTo(2.0D));

    // now some interpolated values
    assertThat(evaluate("apply_continuous(value, 1, exp(2))", tuple), equalTo(1.5D));
    assertThat(evaluate("apply_continuous(value, 2, exp(2))", tuple), equalTo(1.5D));
    assertThat(evaluate("apply_continuous(value, 1, exp(4))", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 2, exp(4))", tuple), equalTo(2.0D));

    // outside of sample range
    // low values
    assertThat(evaluate("apply_continuous(value, 0, exp(0))", tuple), equalTo(1.0D));
    assertThat(evaluate("apply_continuous(value, -10, exp(-10))", tuple), equalTo(1.0D));
    // high values we expect max_y/2 + 0.5 = 3.0
    assertThat(evaluate("apply_continuous(value, 10, exp(10))", tuple), equalTo(2.0D));
    assertThat(evaluate("apply_continuous(value, 20, exp(20))", tuple), equalTo(2.0D));
  }

  @Test
  public void canBuildAndApplyAFunctionWithOneXandYValue() throws Exception {
    // this is kinda dumb but is here to proof that it does work right
    evaluate("create_continuous_2d([1], [2], (x, y) -> x * 10 + y)", tuple("{}"));
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    assertThat(evaluate("apply_continuous(value, 1, 1)", tuple), equalTo(12.0D));
    assertThat(evaluate("apply_continuous(value, 2, 1)", tuple), equalTo(12.0D));
    assertThat(evaluate("apply_continuous(value, 100, 100)", tuple), equalTo(12.0D));
    assertThat(evaluate("apply_continuous(value, -100, -100)", tuple), equalTo(12.0D));
  }

  @Test
  public void canBuildAndApplyABasicFunctionVariesXandY() throws Exception {
    evaluate(
        "create_continuous_2d([1, 3, 5, 7, 9], [1, 2, 3, 4, 5], (x, y) -> (y * 10) + ((x/2) + 0.5))",
        tuple("{}")
    );
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));

    // check that the index/xy code is symetrical
    assertThat(functionType.indexToXY(0), is(new XY(1.0, 1.0)));
    assertThat(functionType.xyToIndex(1.0, 1.0), is(0));
    assertThat(functionType.indexToXY(4), is(new XY(9.0, 1.0)));
    assertThat(functionType.xyToIndex(9.0, 1.0), is(4));
    assertThat(functionType.indexToXY(5), is(new XY(1.0, 2.0)));
    assertThat(functionType.xyToIndex(1.0, 2.0), is(5));
    assertThat(functionType.indexToXY(9), is(new XY(9.0, 2.0)));
    assertThat(functionType.xyToIndex(9.0, 2.0), is(9));
    assertThat(functionType.indexToXY(10), is(new XY(1.0, 3.0)));
    assertThat(functionType.xyToIndex(1.0, 3.0), is(10));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // lets test some points that we have
    assertThat(evaluate("apply_continuous(value, 1, 1)", tuple), equalTo(11.0D));
    assertThat(evaluate("apply_continuous(value, 1, 2)", tuple), equalTo(21.0D));

    assertThat(evaluate("apply_continuous(value, 3, 1)", tuple), equalTo(12.0D));
    assertThat(evaluate("apply_continuous(value, 3, 2)", tuple), equalTo(22.0D));

    assertThat(evaluate("apply_continuous(value, 5, 1)", tuple), equalTo(13.0D));
    assertThat(evaluate("apply_continuous(value, 5, 2)", tuple), equalTo(23.0D));

    // now some interpolated values (x only)
    assertThat(evaluate("apply_continuous(value, 2, 1)", tuple), equalTo(1.5D + 10D));
    assertThat(evaluate("apply_continuous(value, 4, 1)", tuple), equalTo(2.5D + 10D));
    assertThat(evaluate("apply_continuous(value, 6, 1)", tuple), equalTo(3.5D + 10D));
    // now some interpolated values (y only)
    assertThat(evaluate("apply_continuous(value, 1, 1.25)", tuple), equalTo(1D + 12.5D));
    assertThat(evaluate("apply_continuous(value, 1, 1.5)", tuple), equalTo(1D + 15D));
    assertThat(evaluate("apply_continuous(value, 1, 1.75)", tuple), equalTo(1D + 17.5D));

    // now some interpolated values (x and y)
    assertThat(evaluate("apply_continuous(value, 2, 1.25)", tuple), equalTo(1.5D + 12.5D));
    assertThat(evaluate("apply_continuous(value, 2, 1.5)", tuple), equalTo(1.5D + 15D));
    assertThat(evaluate("apply_continuous(value, 2, 1.75)", tuple), equalTo(1.5D + 17.5D));
    assertThat(evaluate("apply_continuous(value, 2.5, 1.25)", tuple), equalTo(1.75D + 12.5D));
    assertThat(evaluate("apply_continuous(value, 2.5, 1.5)", tuple), equalTo(1.75D + 15D));
    assertThat(evaluate("apply_continuous(value, 2.5, 1.75)", tuple), equalTo(1.75D + 17.5D));

    // outside of sample range
    // low x
    assertThat(evaluate("apply_continuous(value, 0, 1.25)", tuple), equalTo(1D + 12.5D));
    assertThat(evaluate("apply_continuous(value, 0, 1.5)", tuple), equalTo(1D + 15D));
    assertThat(evaluate("apply_continuous(value, 0, 1.75)", tuple), equalTo(1D + 17.5D));
    // high x
    assertThat(evaluate("apply_continuous(value, 10, 1.25)", tuple), equalTo(5D + 12.5D));
    assertThat(evaluate("apply_continuous(value, 10, 1.5)", tuple), equalTo(5D + 15D));
    assertThat(evaluate("apply_continuous(value, 10, 1.75)", tuple), equalTo(5D + 17.5D));
    // low y
    assertThat(evaluate("apply_continuous(value, 2, 0)", tuple), equalTo(1.5D + 10D));
    assertThat(evaluate("apply_continuous(value, 4, 0)", tuple), equalTo(2.5D + 10D));
    assertThat(evaluate("apply_continuous(value, 6, -10)", tuple), equalTo(3.5D + 10D));
    // high y
    assertThat(evaluate("apply_continuous(value, 2, 10)", tuple), equalTo(1.5D + 50D));
    assertThat(evaluate("apply_continuous(value, 4, 1000)", tuple), equalTo(2.5D + 50D));
    assertThat(evaluate("apply_continuous(value, 6, 10000)", tuple), equalTo(3.5D + 50D));

    // low low
    assertThat(evaluate("apply_continuous(value, 0, 0)", tuple), equalTo(1D + 10D));
    // low high
    assertThat(evaluate("apply_continuous(value, 0, 10)", tuple), equalTo(1D + 50D));
    // high low
    assertThat(evaluate("apply_continuous(value, 10, 0)", tuple), equalTo(5D + 10D));
    // high high
    assertThat(evaluate("apply_continuous(value, 10, 10)", tuple), equalTo(5D + 50D));
  }

  @Test
  public void canBuildAndApplyABasicFunctionWithMixedNumberTypes() throws Exception {
    evaluate(
        "create_continuous_2d([1, 3.0, 5.0, 7, 9], [1.0, 2, 3, 4, 5.5], (x, y) -> (y * 10) + ((x/2) + 0.5))",
        tuple("{}")
    );
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));

    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // lets test some points that we have
    assertThat(evaluate("apply_continuous(value, 1, 1)", tuple), equalTo(11.0D));
    assertThat(evaluate("apply_continuous(value, 1, 2)", tuple), equalTo(21.0D));
  }

  @Test
  public void canShortCircuitZeroLosses() {
    DummyFunction dummy = new DummyFunction(Arrays.asList(Types.FLOATING, Types.FLOATING)) {
      @Override
      public Type getReturnType() {
        return Types.FLOATING;
      }

      @Override
      public Object call(List<Object> args) {
        double x = (double)args.get(0);
        double y = (double)args.get(1);
        if (! (x == 2 && y == 2) && (x <= 2 || y <= 2)) {
          // boom if x or y are 2 or below (but 2,2 doesn't go boom)
          throw new RuntimeException("boom");
        }
        return x + y - 4;
      }

    };
    project.getFunctionSet().add(dummy);
    evaluate(
        "create_continuous_2d([0, 1, 2, 3, 4], [0, 1, 2, 3, 4], (x, y) -> dummy(x,y), {zero_loss: 0.0})",
        tuple("{}")
    );
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Types.FLOATING));
    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // sanity check that low values make the function go boom
    assertThrows(RuntimeException.class, () -> evaluate("apply_continuous(value, 2, 1)", tuple));
    assertThrows(RuntimeException.class, () -> evaluate("apply_continuous(value, 1, 2)", tuple));
    assertThrows(RuntimeException.class, () -> evaluate("apply_continuous(value, 1, 1)", tuple));

    // first up lets sample at 1.8,1.8. this will need to interpolate between 1 1, 1 2, 2 2, 2 1
    // we test this first to ensure that 2 2 is sampled first (the only sample that doesn't go boom)
    // to ensure that the highest value point is sampled first
    assertThat(evaluate("apply_continuous(value, 1.8, 1.8)", tuple), equalTo(0D));

    // now that the curve is primed we should be able to evauluate it with some smaller z/y values
    // that would cause the dummy function to go boom (if they got that far)
    assertThat(evaluate("apply_continuous(value, 1, 2)", tuple), equalTo(0D));
    assertThat(evaluate("apply_continuous(value, 2, 1)", tuple), equalTo(0D));
    assertThat(evaluate("apply_continuous(value, 2, 1.9)", tuple), equalTo(0D));
    assertThat(evaluate("apply_continuous(value, 0.5, 0.4)", tuple), equalTo(0D));
  }

  @Test
  public void canShortCircuitZeroLossesComplexTypes() {
    DummyFunction dummy = new DummyFunction(Arrays.asList(Types.FLOATING, Types.FLOATING)) {
      Struct returnType = Struct.of("foo", Types.FLOATING);
      @Override
      public Type getReturnType() {
        return returnType;
      }

      @Override
      public Object call(List<Object> args) {
        double x = (double)args.get(0);
        double y = (double)args.get(1);
        if (x < 2 || y < 2) {
          throw new RuntimeException("boom");
        }
        return Tuple.ofValues(returnType, x + y - 4);
      }

    };
    project.getFunctionSet().add(dummy);
    evaluate(
        "create_continuous_2d([1, 2, 3, 4], [1, 2, 3, 4], (x, y) -> dummy(x,y), {zero_loss: {foo: 0}})",
        tuple("{}")
    );
    assertThat(evaluated, isA(BilinearContinuousState.class));
    BilinearContinuousState function = (BilinearContinuousState) evaluated;

    assertThat(realized.getResultType(), isA(BilinearContinuousFunctionType.class));
    BilinearContinuousFunctionType functionType = (BilinearContinuousFunctionType) realized.getResultType();

    assertThat(functionType.getReturnType(), equalTo(Struct.of("foo", Types.FLOATING)));
    Tuple tuple = Tuple.ofValues(functionType.asStruct(), function);

    // prime the curve with some known points
    assertThat(evaluate("apply_continuous(value, 2, 2)", tuple), equalTo(tuple("{foo: 0.0}")));

    // now that the curve is primed we should be able to evauluate it with some smaller z/y values
    // that would cause the dummy function to go boom (if they got that far)
    assertThat(evaluate("apply_continuous(value, 1, 2)", tuple), equalTo(tuple("{foo: 0.0}")));
    assertThat(evaluate("apply_continuous(value, 2, 1)", tuple), equalTo(tuple("{foo: 0.0}")));
    assertThat(evaluate("apply_continuous(value, 1.9, 2)", tuple), equalTo(tuple("{foo: 0.0}")));
    assertThat(evaluate("apply_continuous(value, 2, 1.9)", tuple), equalTo(tuple("{foo: 0.0}")));
  }

  @Test
  public void failsIfZeroLossBadType() {
    Struct returnType = Struct.of("foo", Types.FLOATING);
    DummyFunction dummy = new DummyFunction(Arrays.asList(Types.FLOATING, Types.FLOATING)) {
      @Override
      public Type getReturnType() {
        return returnType;
      }
    };
    project.getFunctionSet().add(dummy);

    evaluate("create_continuous_2d([0,1],[1,2], (x, y) -> dummy(x, y), options: {zero_loss: 'none'})",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        is(TypeProblems.get().mismatch("zero_loss", returnType, Types.TEXT))
    )));
  }

  @Test
  public void functionsCanBeStackedViaAggregateFunction() throws Exception {
    StackContinuousFunction aggregateFunction = new StackContinuousFunction();
    Struct inputType = Struct.of("alpha", Types.INTEGER);

    FunctionCall fc =
        parse("stack_continuous(create_continuous_2d([1, 3, 5], [2, 4], (x, y) -> {a: x * alpha, b: y + alpha}))")
        .isA(FunctionCall.class).get();


    ResultOrProblems<RealizedAggregateExpression> exprOr = aggregateFunction.realize(realizationContext, inputType, fc);
    assertThat(exprOr, Matchers.result(not(nullValue())));

    RealizedAggregateExpression agg = exprOr.get();
    Accumulator acc = agg.newAccumulator();

    acc.accumulate(tuple("{alpha: 10}"));
    acc.accumulate(tuple("{alpha: 20}"));

    BilinearContinuousState stacked = (BilinearContinuousState) acc.process();

    // spot check some values at known points (this is dipping into the function internals here
    // TODO figure out how to get the stacked function type
//    assertThat(stacked.getValue(0), is(tuple("{a: 30.0, b: 34.0}"))); // a: 10 + 20, b: 12 + 22
//    assertThat(stacked.getValue(4), is(tuple("{a: 90.0, b: 38.0}"))); // a: 30 + 60, b: 14 + 24

    // for good measure, let's call the function on it and prove it still works
    Tuple applyInput = Tuple.ofValues(Struct.of("function", agg.getResultType()), stacked);
    evaluate("apply_continuous(function, 1.5, 2.5)", applyInput);
    assertThat(realized.getResultType(), is(Struct.of("a", Types.FLOATING, "b", Types.FLOATING)));

    // smaller than x/y values returns lowest point
    assertThat(evaluate("apply_continuous(function, 0.4, 0.4)", applyInput), is(tuple("{a: 30.0, b: 34.0}")));
    // bigger than x/y values returns largest point
    assertThat(evaluate("apply_continuous(function, 20.0, 30.0)", applyInput), is(tuple("{a: 150.0, b: 38.0}")));

    // somewhere in the middle of x/y points
    assertThat(evaluate("apply_continuous(function, 2.0, 3.0)", applyInput), is(tuple("{a: 60.0, b: 36.0}")));
  }

  @Test
  public void createFailsIfTheLambdaFails() throws Exception {
    evaluate("create_continuous_2d([0,1],[1,2], (x, y) -> call_me(x))", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(Problems.foundWith("apply-to"))
    )));
  }

  @Test
  public void createFailsIfTheXValuesAreNotNumbers() throws Exception {
    evaluate("create_continuous_2d(['a', 'b'], [1,2,4], (x, y) -> 1)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        is(ArgsProblems.mismatch(arguments.get("x-values"), RSList.create(Types.TEXT)))
    )));

    evaluate("create_continuous_2d([1,2,4], ['a', 'b'], (x, y) -> 1)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        is(ArgsProblems.mismatch(arguments.get("y-values"), RSList.create(Types.TEXT)))
    )));
  }

  @Test
  public void createFailsIfXYValuesAreEmpty() throws Exception {
    evaluate("create_continuous_2d([], [1,2,4], (x, y) -> 1)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        is(ArgsProblems.mismatch(arguments.get("x-values"), EmptyList.INSTANCE))
    )));

    evaluate("create_continuous_2d([1,2,4], [], (x, y) -> 1)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        is(ArgsProblems.mismatch(arguments.get("y-values"), EmptyList.INSTANCE))
    )));
  }

  @Test
  public void createFailsIfTheYValuesCanNotBeAddedAndMultiplied() throws Exception {
    evaluate("create_continuous_2d([1, 2, 3], [1,2,3], (x, y) -> 'a')", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(
        CreateContinuousFunction.PROBLEMS.couldNotCreateContinuousFunctionFromYValue(parse("'a'"), Types.TEXT)
      )
    )));

    evaluate("create_continuous_2d([1, 2, 3], [1,2,3], (x, y) -> {a: 'a', b: x})", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(
        CreateContinuousFunction.PROBLEMS.couldNotCreateContinuousFunctionFromYValue(
            parse("{a: 'a', b: x}"), Struct.of("a", Types.TEXT, "b", Types.FLOATING)
        )
      )
    )));
  }

  @Test
  public void createFailsIfThereAreExtraArguments() throws Exception {
    evaluate("create_continuous_2d([1, 2, 3], [1, 2, 3], (x, y) -> x, {apply_log_to_x: true}, 'bob')",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(
          ArgsProblems.get().wrongNumberRange(3, 4, 5)
      )
    )));
  }

  @Test
  public void createFailsIfThereAreNotEnoughArguments() throws Exception {
    evaluate("create_continuous_2d([1, 2, 3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(
          ArgsProblems.get().wrongNumberRange(3, 4, 1)
      )
    )));

    evaluate("create_continuous_2d([1, 2, 3], [1,2,3])", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(
          ArgsProblems.get().wrongNumberRange(3, 4, 2)
      )
    )));
  }

  @Test
  public void createFailsIfLambdaHasWrongArguments() throws Exception {
    FunctionArgument applyToArg = arguments.get("apply-to");

    // too many
    evaluate("create_continuous_2d([1, 2, 3], [1,2,3], (x, y, z) -> x * x + 1)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(
          ArgsProblems.mismatch(applyToArg, new ScopedLambdaType(Struct.EMPTY_STRUCT, "x", "y", "z"))
      )
    )));

    // not enough
    evaluate("create_continuous_2d([1, 2, 3], [1,2,3], (x) -> x * x + 1)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(
          ArgsProblems.mismatch(applyToArg, new ScopedLambdaType(Struct.EMPTY_STRUCT, "x"))
      )
    )));

    // not enough
    evaluate("create_continuous_2d([1, 2, 3], [1,2,3], () -> 1)", Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      Matchers.equalIgnoringChildren(
          ArgsProblems.mismatch(applyToArg, new ScopedLambdaType(Struct.EMPTY_STRUCT))
      )
    )));
  }

  @Test
  public void theLambdaExpressionClosesOverInputScope() throws Exception {
    Object function = evaluate(
        "create_continuous_2d([0, 1, 2, 3, 5, 10], [1,2,3,4,5], (x, y) -> (x + y) * foo)",
        tuple("{foo: 2}")
    );
    Tuple tuple = Tuple.ofValues(realized.getResultType().asStruct(), function);
    assertThat(evaluate("apply_continuous(value, 1, 1)", tuple), equalTo(4.0D));
  }

  @Test
  public void applyFailsIfXYBad() throws Exception {
    // apply_continuous bad x
    evaluate(
        "apply_continuous(create_continuous_2d([1, 2, 3], [1,2,3], (x, y) -> x * x + 1), 'llama', 2)",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        Matchers.equalIgnoringChildren(ArgsProblems.mismatch(applyArguments.get("x-value"), Types.TEXT))
    )));

    // apply_continuous bad y
    evaluate(
        "apply_continuous(create_continuous_2d([1, 2, 3], [1,2,3], (x, y) -> x * x + 1), 2, 'llama')",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        Matchers.equalIgnoringChildren(ArgsProblems.mismatch(applyArguments.get("y-value"), Types.TEXT))
    )));

    // null x
    evaluate(
        "apply_continuous(create_continuous_2d([1, 2, 3], [1,2,3], (x, y) -> x * x + 1), null_of('floating'), 2)",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        Matchers.equalIgnoringChildren(ArgsProblems.mismatch(applyArguments.get("x-value"), Nullable.FLOATING))
    )));

    // null y
    evaluate(
        "apply_continuous(create_continuous_2d([1, 2, 3], [1,2,3], (x, y) -> x * x + 1), 2, null_of('floating'))",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
        Matchers.equalIgnoringChildren(ArgsProblems.mismatch(applyArguments.get("y-value"), Nullable.FLOATING))
    )));

    // apply_continuous mising y
    evaluate(
        "apply_continuous(create_continuous_2d([1, 2, 3], [1,2,3], (x, y) -> x * x + 1), 2)",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      is(ArgsProblems.get().wrongNumber(3, 2))
    )));

    // apply_continuous too many args
    evaluate(
        "apply_continuous(create_continuous_2d([1, 2, 3], [1,2,3], (x, y) -> x * x + 1), 2, 3, 4)",
        Tuple.EMPTY_TUPLE);
    assertThat(realizationProblems, contains(Matchers.hasAncestorProblem(
      is(ArgsProblems.get().wrongNumberRange(2, 3, 4))
    )));
  }

}
